From 72af8166b79973ed29f1b5866d71849ade4e2824 Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Fri, 16 Oct 2026 17:18:44 +0100 Subject: [PATCH] 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. --- .../ingestion/languages/r/simple-hooks.ts | 5 ++- .../r/r-merge-bindings.test.ts | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 gitnexus/test/unit/scope-resolution/r/r-merge-bindings.test.ts diff --git a/gitnexus/src/core/ingestion/languages/r/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/r/simple-hooks.ts index bc81c30da..f95e5a101 100644 --- a/gitnexus/src/core/ingestion/languages/r/simple-hooks.ts +++ b/gitnexus/src/core/ingestion/languages/r/simple-hooks.ts @@ -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 diff --git a/gitnexus/test/unit/scope-resolution/r/r-merge-bindings.test.ts b/gitnexus/test/unit/scope-resolution/r/r-merge-bindings.test.ts new file mode 100644 index 000000000..859ad8df4 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/r/r-merge-bindings.test.ts @@ -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']); + }); +});