mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +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.
188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
/**
|
|
* Unit tests for `diffResolutions` (RFC #909 Ring 2 SHARED #918).
|
|
*
|
|
* Pins the 5 `ShadowAgreement` outcomes and the symmetric-by-kind evidence-
|
|
* delta contract. Inputs are pure data fixtures — no real pipeline state.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
diffResolutions,
|
|
type Resolution,
|
|
type ResolutionEvidence,
|
|
type ShadowCallsite,
|
|
type SymbolDefinition,
|
|
} from 'gitnexus-shared';
|
|
|
|
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
|
|
const callsite: ShadowCallsite = {
|
|
filePath: 'src/app.ts',
|
|
line: 42,
|
|
col: 8,
|
|
calledName: 'save',
|
|
};
|
|
|
|
const makeDef = (nodeId: string): SymbolDefinition => ({
|
|
nodeId,
|
|
filePath: 'src/models.ts',
|
|
type: 'Method',
|
|
});
|
|
|
|
const makeEvidence = (kind: ResolutionEvidence['kind'], weight = 0.5): ResolutionEvidence => ({
|
|
kind,
|
|
weight,
|
|
});
|
|
|
|
const makeResolution = (
|
|
nodeId: string,
|
|
evidenceKinds: readonly ResolutionEvidence['kind'][],
|
|
): Resolution => ({
|
|
def: makeDef(nodeId),
|
|
confidence: Math.min(1, evidenceKinds.length * 0.3),
|
|
evidence: evidenceKinds.map((k) => makeEvidence(k)),
|
|
});
|
|
|
|
// ─── Agreement outcomes ─────────────────────────────────────────────────────
|
|
|
|
describe('diffResolutions — agreement outcomes', () => {
|
|
it("both arrays empty → 'both-empty' with no evidence delta", () => {
|
|
const result = diffResolutions(callsite, [], []);
|
|
expect(result.agreement).toBe('both-empty');
|
|
expect(result.evidenceDelta).toEqual([]);
|
|
expect(result.legacy).toBeNull();
|
|
expect(result.newResult).toBeNull();
|
|
});
|
|
|
|
it("identical top DefIds → 'both-agree' with empty evidence delta", () => {
|
|
const legacy = [makeResolution('def:User.save', ['local', 'owner-match'])];
|
|
const next = [makeResolution('def:User.save', ['local', 'kind-match'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
expect(result.agreement).toBe('both-agree');
|
|
expect(result.evidenceDelta).toEqual([]);
|
|
expect(result.legacy).toBe(legacy[0]);
|
|
expect(result.newResult).toBe(next[0]);
|
|
});
|
|
|
|
it("legacy empty, new non-empty → 'only-new' with new's evidence as delta", () => {
|
|
const next = [makeResolution('def:User.save', ['local', 'owner-match'])];
|
|
const result = diffResolutions(callsite, [], next);
|
|
expect(result.agreement).toBe('only-new');
|
|
expect(result.evidenceDelta).toEqual(next[0].evidence);
|
|
expect(result.legacy).toBeNull();
|
|
expect(result.newResult).toBe(next[0]);
|
|
});
|
|
|
|
it("legacy non-empty, new empty → 'only-legacy' with legacy's evidence as delta", () => {
|
|
const legacy = [makeResolution('def:User.save', ['global-name'])];
|
|
const result = diffResolutions(callsite, legacy, []);
|
|
expect(result.agreement).toBe('only-legacy');
|
|
expect(result.evidenceDelta).toEqual(legacy[0].evidence);
|
|
expect(result.legacy).toBe(legacy[0]);
|
|
expect(result.newResult).toBeNull();
|
|
});
|
|
|
|
it("different top DefIds → 'both-disagree'", () => {
|
|
const legacy = [makeResolution('def:ModelA.save', ['global-name'])];
|
|
const next = [makeResolution('def:ModelB.save', ['local'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
expect(result.agreement).toBe('both-disagree');
|
|
expect(result.legacy).toBe(legacy[0]);
|
|
expect(result.newResult).toBe(next[0]);
|
|
});
|
|
});
|
|
|
|
// ─── Evidence delta — symmetric difference by `kind` ────────────────────────
|
|
|
|
describe('diffResolutions — evidence delta (symmetric-by-kind)', () => {
|
|
it("'both-disagree' with disjoint evidence → delta contains both sides' kinds", () => {
|
|
const legacy = [makeResolution('def:A', ['global-name'])];
|
|
const next = [makeResolution('def:B', ['local', 'owner-match'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
expect(result.evidenceDelta.map((e) => e.kind)).toEqual([
|
|
'global-name',
|
|
'local',
|
|
'owner-match',
|
|
]);
|
|
});
|
|
|
|
it("'both-disagree' with overlapping kinds → overlapping kinds removed from delta", () => {
|
|
const legacy = [makeResolution('def:A', ['local', 'scope-chain', 'global-name'])];
|
|
const next = [makeResolution('def:B', ['local', 'import', 'owner-match'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
// 'local' is on both sides → dropped
|
|
// Remaining: legacy-only ['scope-chain', 'global-name'], then new-only ['import', 'owner-match']
|
|
expect(result.evidenceDelta.map((e) => e.kind)).toEqual([
|
|
'scope-chain',
|
|
'global-name',
|
|
'import',
|
|
'owner-match',
|
|
]);
|
|
});
|
|
|
|
it("'both-disagree' with fully overlapping kinds → empty evidence delta", () => {
|
|
const legacy = [makeResolution('def:A', ['local', 'owner-match'])];
|
|
const next = [makeResolution('def:B', ['owner-match', 'local'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
// Same kind set, different order → symmetric difference is empty
|
|
expect(result.evidenceDelta).toEqual([]);
|
|
expect(result.agreement).toBe('both-disagree'); // agreement still disagrees because nodeIds differ
|
|
});
|
|
|
|
it('differing weights on the same kind → NOT a delta (keyed on kind only)', () => {
|
|
const legacy = [
|
|
{
|
|
def: makeDef('def:A'),
|
|
confidence: 0.9,
|
|
evidence: [{ kind: 'local' as const, weight: 0.55 }],
|
|
},
|
|
];
|
|
const next = [
|
|
{
|
|
def: makeDef('def:B'),
|
|
confidence: 0.1,
|
|
evidence: [{ kind: 'local' as const, weight: 0.25 }],
|
|
},
|
|
];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
expect(result.agreement).toBe('both-disagree');
|
|
expect(result.evidenceDelta).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ─── Metadata + ordering ────────────────────────────────────────────────────
|
|
|
|
describe('diffResolutions — metadata + ordering', () => {
|
|
it('ignores resolutions beyond index 0 (top match only)', () => {
|
|
const legacy = [
|
|
makeResolution('def:User.save', ['local']),
|
|
makeResolution('def:other', ['global-name']),
|
|
];
|
|
const next = [
|
|
makeResolution('def:User.save', ['local']),
|
|
makeResolution('def:yet-another', ['wildcard']),
|
|
];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
expect(result.agreement).toBe('both-agree');
|
|
});
|
|
|
|
it('preserves callsite verbatim', () => {
|
|
const result = diffResolutions(callsite, [], []);
|
|
expect(result.callsite).toBe(callsite);
|
|
});
|
|
|
|
it("'both-disagree' delta order: legacy-only first (input order), then new-only", () => {
|
|
const legacy = [makeResolution('def:A', ['owner-match', 'scope-chain', 'kind-match'])];
|
|
const next = [makeResolution('def:B', ['import', 'owner-match', 'arity-match'])];
|
|
const result = diffResolutions(callsite, legacy, next);
|
|
// 'owner-match' overlaps → dropped
|
|
// legacy-only in original order: ['scope-chain', 'kind-match']
|
|
// then new-only in original order: ['import', 'arity-match']
|
|
expect(result.evidenceDelta.map((e) => e.kind)).toEqual([
|
|
'scope-chain',
|
|
'kind-match',
|
|
'import',
|
|
'arity-match',
|
|
]);
|
|
});
|
|
});
|