0011 The Sources list only ever shows what the answer actually cited¶
Status¶
Accepted
Context¶
A retrieved source can survive scoring, reranking, and the entity cap without
ever being cited in the generated text — the model may ground its reasoning
in a chunk without quoting it, or ignore a source entirely. Surfacing every
retrieved-and-above-threshold source in the client's Sources list regardless
produced answers where, for example, [1] and [3] were cited in the text
but [2] still appeared in the Sources list, unlinked to anything.
On the SSE path this is compounded by ADR 0005:
the references event fires before generation completes, so it can only ever
list every retrieved source — it cannot yet know which ones the text will
end up citing.
Decision¶
AnswerService::finalizeCitations() runs after generation completes, on both
delivery paths. It drops any source with no surviving [n] marker in the
text and renumbers the remainder in first-citation order (so [1][3]
becomes [1][2]), rewriting the markers to match. It falls back to the
untouched list only when the text cites nothing at all, so a provider that
ignores the citation contract doesn't leave a grounded answer with zero
sources.
On the JSON path this is the only Sources list the client ever sees. On the
SSE path, the early references event still lists every retrieved source
(for responsiveness — the client can render something before generation
finishes); the done event separately carries the corrected text and
references pair, and ai_answers.answer.js swaps to that corrected pair
once done arrives.
Consequences¶
- Two different reference lists exist in flight on the SSE path — the early,
uncorrected one and the final, corrected one — and any front-end change to
reference rendering has to account for both events, not just
references. stripOutOfRangeMarkers()must run beforefinalizeCitations(): it clears out-of-range[n]markers first sofinalizeCitations()'s citation count only ever sees valid 1-based indices into$sourcesUsed.