fix: resolve a citation by its number, not its position in the array

A marker names a source by the `number` dedupeSources assigns server-side.
Rendering looked it up as sources[n - 1], which works only while the array
order and the numbers agree. Nothing breaks that today, but it is an implicit
contract across a network boundary: any later filtering or reordering of the
list — hiding low-score sources, say — would point citations at the wrong
source silently, which is worse than not linking at all.

Matching on the number cannot drift. Positional lookup remains as the fallback
for a list whose entries carry no number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-10 19:10:54 +02:00
parent e2444066a0
commit 6dcdf36c81

View file

@ -88,6 +88,20 @@ export function wrapTables(html) {
.replace(/<\/table>/g, '</table></div>');
}
// A citation marker names a source by its `number`, which dedupeSources assigns
// server-side. Resolving it by array position happens to work today because the
// two agree, but any future filtering or reordering of the list between the
// server and here would silently point citations at the wrong source. Matching
// on the number cannot drift.
function sourceByNumber(sources, n) {
var list = sources || [];
for (var i = 0; i < list.length; i++) {
if (list[i] && Number(list[i].number) === Number(n)) return list[i];
}
// Sources predating the numbering, or a caller passing a bare list.
return list[n - 1];
}
export function renderCitationLinks(html, sources, options) {
var opts = options || {};
return String(html || '').replace(new RegExp(
@ -95,9 +109,9 @@ export function renderCitationLinks(html, sources, options) {
'gi'), function (match, tag, cluster) {
if (!cluster) return match;
var nums = cluster.split(',').map(function (n) { return Number(n.trim()); }).filter(function (n) { return Number.isInteger(n) && n > 0; });
if (!nums.length || nums.some(function (n) { return !sources[n - 1]; })) return match;
if (!nums.length || nums.some(function (n) { return !sourceByNumber(sources, n); })) return match;
return nums.map(function (n) {
var source = sources[n - 1];
var source = sourceByNumber(sources, n);
var title = source ? source.title || source.resource || 'Source' : 'Source';
var page = source && (source.page || source.page_number || source.pageNumber);
var label = 'Source ' + n + ': ' + title + (page ? ', page ' + page : '');
@ -136,7 +150,7 @@ function parseCitationCluster(cluster) {
}
function allCitationsAvailable(nums, sources) {
return nums.length > 0 && nums.every(function(n) { return sources[n - 1]; });
return nums.length > 0 && nums.every(function(n) { return sourceByNumber(sources, n); });
}
function formatCitationCluster(nums) {