mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
Replaces the scaffold stubs with working pure-logic implementations plus
unit-test coverage for both functions. Unblocks Ring 2 PKG #923 (shadow
harness) to consume a concrete library instead of throwing scaffolds.
gitnexus-shared/src/scope-resolution/shadow/diff.ts
`diffResolutions(callsite, legacy, newResult): ShadowDiff`
- [0] on each side is the top match
- both empty → 'both-empty', delta []
- legacy empty only → 'only-new', delta = new top evidence
- new empty only → 'only-legacy', delta = legacy top evidence
- same top nodeId → 'both-agree', delta []
- different nodeIds → 'both-disagree',
delta = symmetric difference of evidence kinds
(legacy-only first in input order, then new-only)
Evidence identity is `ResolutionEvidence.kind` — weight/note differences
for the same kind do NOT produce delta entries. Rationale: the aggregator
wants to know which *signals* explain a disagreement, not fluctuations
in calibration values.
gitnexus-shared/src/scope-resolution/shadow/aggregate.ts
`aggregateDiffs(diffs, now?): ShadowParityReport`
- buckets by `SupportedLanguages`
- tallies agreements, evidence-breakdown (divergences only — agree and
empty rows do not contribute)
- parity = bothAgree / (totalCalls - bothEmpty), yields 0 (not NaN)
when the denominator is 0
- perLanguage sorted alphabetically by enum value for stable output
- evidenceBreakdown internally sorted by kind for stable output
- overall = column-wise sum across languages
- `now` parameter makes generatedAt deterministic in tests
gitnexus-shared/src/index.ts
Re-exports the full shadow API: diffResolutions, aggregateDiffs, and all
their types (ShadowAgreement, ShadowCallsite, ShadowDiff,
LanguageParityRow, ShadowParityReport).
gitnexus/test/unit/shadow/diff.test.ts (13 tests)
- 5 agreement outcomes
- symmetric-by-kind evidence delta (disjoint, overlapping, fully-overlapping)
- weight-only differences produce no delta
- top-match only (ignores indices beyond [0])
- callsite passthrough
- delta ordering (legacy-only first, input order preserved)
gitnexus/test/unit/shadow/aggregate.test.ts (9 tests)
- empty input
- single language, all agree / mixed / all empty
- multi-language bucketing + overall sum
- alphabetical language sort
- evidence breakdown scope
- determinism via injected `now` + JSON round-trip identity
Verification:
- gitnexus-shared + gitnexus build clean (tsc + scripts/build.js)
- test/unit/shadow: 22/22 pass
- test/unit/model + test/unit/shadow combined: 106/106 pass
- No runtime behavior changes (shadow is invoked by #923, not yet wired)
Stacked on main (af1d278a). Depends on types from #910 (merged).
Unblocks: #923 (Ring 2 PKG — shadow harness wiring) — concrete library
to consume instead of scaffold stubs.
Plan: docs/plans/2026-04-18-001-refactor-911-senior-hooks-redesign-plan.md
is about #911; #918's scope is the scaffold+fill-in described in the PR
description of #951.
234 lines
9.2 KiB
TypeScript
234 lines
9.2 KiB
TypeScript
/**
|
|
* Unit tests for `aggregateDiffs` (RFC #909 Ring 2 SHARED #918).
|
|
*
|
|
* Covers bucketing by language, parity math (incl. zero-resolved edge),
|
|
* evidence-kind breakdown, and stable sort order on the output rows.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
aggregateDiffs,
|
|
SupportedLanguages,
|
|
type LanguageParityRow,
|
|
type ResolutionEvidence,
|
|
type ShadowAgreement,
|
|
type ShadowDiff,
|
|
} from 'gitnexus-shared';
|
|
|
|
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
|
|
const FIXED_NOW = new Date('2026-04-18T12:00:00.000Z');
|
|
|
|
const makeDiff = (
|
|
agreement: ShadowAgreement,
|
|
evidenceKinds: readonly ResolutionEvidence['kind'][] = [],
|
|
): ShadowDiff => ({
|
|
callsite: { filePath: 'src/x.ts', line: 1, col: 0, calledName: 'foo' },
|
|
legacy: null,
|
|
newResult: null,
|
|
agreement,
|
|
evidenceDelta: evidenceKinds.map((kind) => ({ kind, weight: 0.3 })),
|
|
});
|
|
|
|
const entry = (language: SupportedLanguages, diff: ShadowDiff) => ({ language, diff });
|
|
|
|
const findRow = (
|
|
rows: readonly LanguageParityRow[],
|
|
language: SupportedLanguages,
|
|
): LanguageParityRow => {
|
|
const row = rows.find((r) => r.language === language);
|
|
if (!row) throw new Error(`no row for ${language}`);
|
|
return row;
|
|
};
|
|
|
|
// ─── Empty input ────────────────────────────────────────────────────────────
|
|
|
|
describe('aggregateDiffs — empty input', () => {
|
|
it('returns empty perLanguage, zeroed overall, generatedAt populated', () => {
|
|
const report = aggregateDiffs([], FIXED_NOW);
|
|
expect(report.perLanguage).toEqual([]);
|
|
expect(report.overall).toEqual({
|
|
totalCalls: 0,
|
|
bothAgree: 0,
|
|
onlyLegacy: 0,
|
|
onlyNew: 0,
|
|
bothDisagree: 0,
|
|
bothEmpty: 0,
|
|
parity: 0,
|
|
});
|
|
expect(report.generatedAt).toBe('2026-04-18T12:00:00.000Z');
|
|
});
|
|
});
|
|
|
|
// ─── Single language, single outcome ────────────────────────────────────────
|
|
|
|
describe('aggregateDiffs — single language', () => {
|
|
it('all both-agree → parity = 1.0', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Python, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Python, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Python, makeDiff('both-agree')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
expect(report.perLanguage).toHaveLength(1);
|
|
const row = findRow(report.perLanguage, SupportedLanguages.Python);
|
|
expect(row).toMatchObject({
|
|
language: SupportedLanguages.Python,
|
|
totalCalls: 3,
|
|
bothAgree: 3,
|
|
onlyLegacy: 0,
|
|
onlyNew: 0,
|
|
bothDisagree: 0,
|
|
bothEmpty: 0,
|
|
parity: 1,
|
|
});
|
|
});
|
|
|
|
it('mixed outcomes → parity excludes both-empty from denominator', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('only-legacy', ['global-name'])),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('only-new', ['local'])),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-disagree', ['import'])),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-empty')),
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-empty')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
const row = findRow(report.perLanguage, SupportedLanguages.TypeScript);
|
|
expect(row.totalCalls).toBe(7);
|
|
expect(row.bothAgree).toBe(2);
|
|
expect(row.onlyLegacy).toBe(1);
|
|
expect(row.onlyNew).toBe(1);
|
|
expect(row.bothDisagree).toBe(1);
|
|
expect(row.bothEmpty).toBe(2);
|
|
// parity = bothAgree / (totalCalls - bothEmpty) = 2 / (7 - 2) = 0.4
|
|
expect(row.parity).toBeCloseTo(0.4, 10);
|
|
});
|
|
|
|
it('all both-empty → parity = 0 (not NaN)', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Java, makeDiff('both-empty')),
|
|
entry(SupportedLanguages.Java, makeDiff('both-empty')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
const row = findRow(report.perLanguage, SupportedLanguages.Java);
|
|
expect(row.totalCalls).toBe(2);
|
|
expect(row.bothEmpty).toBe(2);
|
|
expect(row.parity).toBe(0);
|
|
expect(Number.isNaN(row.parity)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Multi-language ─────────────────────────────────────────────────────────
|
|
|
|
describe('aggregateDiffs — multiple languages', () => {
|
|
it('buckets rows by language and sums overall column-wise', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Python, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Python, makeDiff('both-disagree', ['local'])),
|
|
entry(SupportedLanguages.Ruby, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Ruby, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Ruby, makeDiff('only-new', ['type-binding'])),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
expect(report.perLanguage).toHaveLength(2);
|
|
|
|
const python = findRow(report.perLanguage, SupportedLanguages.Python);
|
|
expect(python.totalCalls).toBe(2);
|
|
expect(python.bothAgree).toBe(1);
|
|
expect(python.bothDisagree).toBe(1);
|
|
expect(python.parity).toBe(0.5);
|
|
|
|
const ruby = findRow(report.perLanguage, SupportedLanguages.Ruby);
|
|
expect(ruby.totalCalls).toBe(3);
|
|
expect(ruby.bothAgree).toBe(2);
|
|
expect(ruby.onlyNew).toBe(1);
|
|
expect(ruby.parity).toBeCloseTo(2 / 3, 10);
|
|
|
|
expect(report.overall).toEqual({
|
|
totalCalls: 5,
|
|
bothAgree: 3,
|
|
onlyLegacy: 0,
|
|
onlyNew: 1,
|
|
bothDisagree: 1,
|
|
bothEmpty: 0,
|
|
parity: 3 / 5,
|
|
});
|
|
});
|
|
|
|
it('perLanguage rows are sorted alphabetically by language value for stable output', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.TypeScript, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.C, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Python, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Java, makeDiff('both-agree')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
const ordered = report.perLanguage.map((r) => r.language);
|
|
// Alphabetical by enum VALUE: 'c' < 'java' < 'python' < 'typescript'
|
|
expect(ordered).toEqual([
|
|
SupportedLanguages.C,
|
|
SupportedLanguages.Java,
|
|
SupportedLanguages.Python,
|
|
SupportedLanguages.TypeScript,
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ─── Evidence breakdown ─────────────────────────────────────────────────────
|
|
|
|
describe('aggregateDiffs — evidence breakdown', () => {
|
|
it('counts divergence evidence kinds across non-agreeing rows only', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Go, makeDiff('both-disagree', ['import', 'owner-match'])),
|
|
entry(SupportedLanguages.Go, makeDiff('only-legacy', ['import', 'global-name'])),
|
|
entry(SupportedLanguages.Go, makeDiff('only-new', ['local'])),
|
|
// both-agree contributes 0 to evidence breakdown regardless of any attached evidence
|
|
entry(SupportedLanguages.Go, makeDiff('both-agree', ['import'])),
|
|
// both-empty also contributes 0
|
|
entry(SupportedLanguages.Go, makeDiff('both-empty')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
const row = findRow(report.perLanguage, SupportedLanguages.Go);
|
|
expect(Array.from(row.evidenceBreakdown.entries())).toEqual([
|
|
['global-name', 1],
|
|
['import', 2],
|
|
['local', 1],
|
|
['owner-match', 1],
|
|
]);
|
|
});
|
|
|
|
it('emits empty evidenceBreakdown when all calls agree or are empty', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Rust, makeDiff('both-agree')),
|
|
entry(SupportedLanguages.Rust, makeDiff('both-empty')),
|
|
];
|
|
const report = aggregateDiffs(diffs, FIXED_NOW);
|
|
const row = findRow(report.perLanguage, SupportedLanguages.Rust);
|
|
expect(row.evidenceBreakdown.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ─── Determinism ────────────────────────────────────────────────────────────
|
|
|
|
describe('aggregateDiffs — determinism', () => {
|
|
it('injected `now` is used verbatim for generatedAt', () => {
|
|
const t = new Date('2030-01-01T00:00:00.000Z');
|
|
const report = aggregateDiffs([], t);
|
|
expect(report.generatedAt).toBe('2030-01-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('same input produces byte-identical JSON (stable keys + sort)', () => {
|
|
const diffs = [
|
|
entry(SupportedLanguages.Python, makeDiff('both-disagree', ['local', 'import'])),
|
|
entry(SupportedLanguages.Java, makeDiff('both-agree')),
|
|
];
|
|
const a = aggregateDiffs(diffs, FIXED_NOW);
|
|
const b = aggregateDiffs(diffs, FIXED_NOW);
|
|
// Round-trip through JSON to drop Map identity and force structural comparison.
|
|
const toJson = (r: typeof a): string =>
|
|
JSON.stringify(r, (_key, v: unknown) => (v instanceof Map ? Object.fromEntries(v) : v));
|
|
expect(toJson(a)).toBe(toJson(b));
|
|
});
|
|
});
|