fix: citation chips that sit together read in ascending order
[1] early in an answer and then "[2][1]" later showed "2 1". A run of adjacent chips is now sorted by what it displays — a core rule after inline parsing — so it reads "1 2". Text between two clusters keeps them apart, every chip still points at its own source, and the numbering itself is unchanged: only the order within a run moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
ed17d38229
commit
6e779e61ff
2 changed files with 45 additions and 3 deletions
|
|
@ -78,13 +78,41 @@ function displayNumber(env, n) {
|
|||
return i + 1;
|
||||
}
|
||||
|
||||
// What a chip shows. A list ordered by citation already says what the reader
|
||||
// should see, and the panel shows the same number; otherwise, first
|
||||
// appearance in this text.
|
||||
function displayFor(env, meta) {
|
||||
var source = sourceByNumber((env && env.assistantSources) || [], meta.source);
|
||||
return source && source.sourceNumber != null ? source.number : meta.display;
|
||||
}
|
||||
|
||||
// Chips that sit together read in order. "[2][1]" written by the model after
|
||||
// [1] appeared earlier showed "2 1"; the run is sorted by what it displays, so
|
||||
// it reads "1 2". Only runs of adjacent chips are touched — text between two
|
||||
// clusters keeps them apart — and every chip still points at its own source,
|
||||
// so nothing about accuracy changes.
|
||||
function sortCitationRuns(state) {
|
||||
state.tokens.forEach(function (block) {
|
||||
if (block.type !== 'inline' || !block.children) return;
|
||||
var kids = block.children, i = 0;
|
||||
while (i < kids.length) {
|
||||
if (kids[i].type !== 'assistant_cite') { i++; continue; }
|
||||
var j = i;
|
||||
while (j < kids.length && kids[j].type === 'assistant_cite') j++;
|
||||
if (j - i > 1) {
|
||||
var run = kids.slice(i, j).sort(function (a, b) { return displayFor(state.env, a.meta) - displayFor(state.env, b.meta); });
|
||||
for (var k = 0; k < run.length; k++) kids[i + k] = run[k];
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderCitation(tokens, idx, o, env) {
|
||||
var meta = tokens[idx].meta;
|
||||
var opts = (env && env.assistantOpts) || {};
|
||||
var source = sourceByNumber((env && env.assistantSources) || [], meta.source);
|
||||
// A list ordered by citation already says what the reader should see, and
|
||||
// the panel shows the same number. Otherwise, first appearance in this text.
|
||||
var display = source && source.sourceNumber != null ? source.number : meta.display;
|
||||
var display = displayFor(env, meta);
|
||||
var title = source ? source.title || source.resource || 'Source' : 'Source';
|
||||
var page = source && (source.page || source.page_number || source.pageNumber);
|
||||
var label = 'Source ' + display + ': ' + title + (page ? ', page ' + page : '');
|
||||
|
|
@ -112,6 +140,7 @@ export function installCitations(md) {
|
|||
md.__assistantCitations = true;
|
||||
// Before 'escape', or \[1\] is consumed as an escaped bracket first.
|
||||
md.inline.ruler.before('escape', 'assistant_cite', citationRule);
|
||||
md.core.ruler.push('assistant_cite_order', sortCitationRuns);
|
||||
md.renderer.rules.assistant_cite = renderCitation;
|
||||
var image = md.renderer.rules.image || function (t, i, o, e, self) { return self.renderToken(t, i, o); };
|
||||
md.renderer.rules.image = function (tokens, idx, o, env, self) {
|
||||
|
|
|
|||
|
|
@ -371,3 +371,16 @@ test('a markdown link or footnote whose text is a bare number stays a link, not
|
|||
assert.equal((html.match(/assistant-cite/g) || []).length, 1);
|
||||
assert.match(html, /data-source-number="2"/);
|
||||
});
|
||||
|
||||
test('chips that sit together read in ascending order, and still point at their own sources', async () => {
|
||||
const { renderAssistantMarkdown } = await loadCitationModule();
|
||||
const four = [1, 2, 3, 4].map(n => ({ number: n, title: 'S' + n }));
|
||||
// [1] appears first; the model then writes [2][1] — which showed "2 1".
|
||||
const html = renderAssistantMarkdown('First claim [1]. Second claim [2][1]. Third [3, 2].', four);
|
||||
const chips = [...html.matchAll(/data-source-number="(\d+)"[^>]*data-display-number="(\d+)"/g)].map(m => m[1] + '→' + m[2]);
|
||||
assert.deepEqual(chips, ['1→1', '1→1', '2→2', '2→2', '3→3'], 'each run ascends by display; identity is untouched');
|
||||
// Clusters separated by text are not merged into one run.
|
||||
const apart = renderAssistantMarkdown('A [2] then [1].', four);
|
||||
const order = [...apart.matchAll(/data-display-number="(\d+)"/g)].map(m => m[1]);
|
||||
assert.deepEqual(order, ['1', '2'], 'first appearance still numbers them; nothing is reordered across text');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue