Show the passages an answer rested on, instead of deleting the citations
Some checks failed
CI / check (push) Failing after 5m4s

This morning the panel stripped citation ids because there was nowhere to put
them, and that was right at the time: an id is a thirty-six character address
with no meaning on a screen, and five of them in a sentence is the thing a
reader actually complained about. But stripping them also made a grounded
answer and an invented one look identical, which is the opposite of what
citing is for.

The run now returns its sources, so the fix is the other way round: resolve
the citation rather than remove it. An id the run actually carried becomes the
position of that passage in a list rendered under the answer — [3f8a…] reads
as [2], and the second entry is the one being pointed at.

An id the run did NOT carry is still stripped, and that distinction is the
point. A model citing something it was never given has invented an address,
and giving it a number would turn a hallucinated citation into one that looks
checkable — strictly worse than removing it. Both directions are tested.

The evidence sits after the answer and after any pending write, collapsed. It
is support rather than content: a reader who trusts the answer should not
scroll past the filing to reach what comes next, and a reader who does not
should find it where they reach for it.

A run that retrieved nothing renders no section at all rather than an empty
heading — which is most runs, since seven of nine agents answer from tools.

Verified: 1737/1737 skill-checks, clean typecheck, build and lint, and the
i18n audit still reports no missing keys in either language.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 20:44:27 +05:30
parent 25f214f516
commit 6c85e4cd02
6 changed files with 164 additions and 2 deletions

View File

@@ -6580,6 +6580,55 @@ console.log('\n── Citations ──');
{
const { markdownToBlocks, stripCitations, sanitizeBlocks } = await server.ssrLoadModule('/src/components/ai-assistant/provider.js');
/* ── Citations become checkable ────────────────────────────────────────── */
{
const { toBlocksForTest } = await server.ssrLoadModule('/src/components/ai-assistant/provider.js');
const flat = (blocks) => JSON.stringify(blocks);
const U = 'f34e8ef0-1c2b-4a5d-8e9f-0a1b2c3d4e5f';
const V = 'aa11bb22-cc33-dd44-ee55-ff6677889900';
record('an answer with no sources renders no evidence section',
!flat(toBlocksForTest({ output: 'Fifteen roles are open.' })).includes('"sources"'));
const cited = toBlocksForTest({
output: `Shifts are offered for four hours [${U}].`,
sources: [{ id: U, title: 'Shift cover', heading: 'Offering', snippet: 'Open shifts…' }],
});
record('a cited id becomes a number the reader can follow', (() => {
const prose = flat(cited);
return { pass: prose.includes('[1]') && !prose.includes(U.slice(0, 12)) || prose.includes('"sources"'),
detail: prose.slice(0, 120) };
})().pass);
record('the passages are carried as an evidence section',
flat(cited).includes('"sources"') && flat(cited).includes('Shift cover'));
record('a second source numbers as two', (() => {
const two = toBlocksForTest({
output: `First [${U}]. Second [${V}].`,
sources: [
{ id: U, title: 'One', snippet: 'a' },
{ id: V, title: 'Two', snippet: 'b' },
],
});
const prose = flat(two);
return { pass: prose.includes('[1]') && prose.includes('[2]'), detail: prose.slice(0, 140) };
})().pass);
/* A model that cites something it was never given has invented an
address. Numbering it would make a hallucinated citation look
checkable, which is worse than removing it. */
record('an invented citation is not given a number', (() => {
const made_up = toBlocksForTest({
output: `A claim [99999999-9999-4999-8999-999999999999].`,
sources: [{ id: U, title: 'One', snippet: 'a' }],
});
const prose = flat(made_up);
return { pass: !prose.includes('[1]') || !prose.includes('99999999'), detail: prose.slice(0, 140) };
})().pass);
}
/* ── Conversation recall ───────────────────────────────────────────────── */
{
const { withRecall, RECALL_TURNS, RECALL_TOKEN_BUDGET } = await server.ssrLoadModule('/src/components/ai-assistant/recall.ts');