fix(r): order merged bindings by code unit, not by locale

rMergeBindings sorted same-tier bindings with localeCompare, whose order
follows the machine's locale. Compare node ids by code unit, as the package
discovery does.
This commit is contained in:
Mick Cooney 2026-10-16 17:18:44 +01:00
parent 33d411c50c
commit 72af8166b7
2 changed files with 40 additions and 1 deletions

View file

@ -28,7 +28,10 @@ export function rMergeBindings(
if ((TIER[b.origin] ?? 99) !== bestTier) continue;
if (!byNode.has(b.def.nodeId)) byNode.set(b.def.nodeId, b);
}
return [...byNode.values()].sort((a, b) => a.def.nodeId.localeCompare(b.def.nodeId));
// By code unit, not `localeCompare`: the order must not depend on the machine's locale.
return [...byNode.values()].sort((a, b) =>
a.def.nodeId < b.def.nodeId ? -1 : a.def.nodeId > b.def.nodeId ? 1 : 0,
);
}
/** R declarations carry no synthesized arity metadata (no `...`/default-arg

View file

@ -0,0 +1,36 @@
/**
* `rMergeBindings` orders the bindings of one tier by node id. The order must be the same on every
* machine, so it compares code units, not `localeCompare`, which follows the locale: in an
* English collation `a` sorts before `B` and `é` before `f`; by code unit `B` and `f` come first.
*/
import { describe, expect, it } from 'vitest';
import type { BindingRef, SymbolDefinition } from 'gitnexus-shared';
import { rMergeBindings } from '../../../../src/core/ingestion/languages/r/simple-hooks.js';
const local = (nodeId: string): BindingRef => ({
origin: 'local',
def: { nodeId } as SymbolDefinition,
});
const order = (...ids: string[]): string[] =>
rMergeBindings(ids.map(local), [], 'scope:test').map((b) => b.def.nodeId);
describe('rMergeBindings', () => {
it('orders same-tier bindings by code unit', () => {
expect(order('Function:a.R:a', 'Function:a.R:B')).toEqual(['Function:a.R:B', 'Function:a.R:a']);
expect(order('Function:a.R:é', 'Function:a.R:f')).toEqual(['Function:a.R:f', 'Function:a.R:é']);
});
it('gives the same order whichever way round the bindings arrive', () => {
expect(order('Function:a.R:B', 'Function:a.R:a')).toEqual(['Function:a.R:B', 'Function:a.R:a']);
});
it('keeps one binding per node and prefers the tightest tier', () => {
const merged = rMergeBindings(
[local('Function:a.R:x'), { origin: 'import', def: { nodeId: 'Function:b.R:x' } as never }],
[local('Function:a.R:x')],
'scope:test',
);
expect(merged.map((b) => b.def.nodeId)).toEqual(['Function:a.R:x']);
});
});