mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* chore(shared): apply Ring 2 SHARED review follow-ups in one diff Aggregates all actionable follow-ups from the 9 Ring 2 SHARED PRs (#949–#963) before proceeding to Ring 2 PKG. No behavior changes; docstring edits, test refinements, and one structural cleanup. ## #913 (DefIndex / ModuleScopeIndex / QualifiedNameIndex) - Rename `freezeIndex` → `wrapIndex` across all three index builders. The old name implied `Object.freeze` on the wrapper, which we never applied; `wrapIndex` more accurately describes the lightweight readonly-interface wrap. Safety surface (frozen bucket arrays, frozen miss-empty array, readonly Maps) is unchanged. - Document in `buildModuleScopeIndex` JSDoc that callers must pre-normalize `filePath` keys (no path-separator canonicalization happens here). Prevents silent cross-platform misses. - Add an explicit hit-path freeze assertion in `qualified-name-index.test.ts` (the existing test covered only the miss-path `EMPTY` array). ## #914 (MethodDispatchIndex) - Differentiate the C3 and BFS test cases: both tests now use distinct MRO orderings so they prove the materializer stores whatever order the `computeMro` callback produces (not that C3 and BFS yield identical output). - Add `implementsOfCalls` counter in the first-write-wins test, and document the call-count contract in `MethodDispatchInput.implementsOf` JSDoc: `implementsOf` fires **per occurrence** in `input.owners` (not per unique owner); `computeMro` fires at most once per unique owner. Callers with expensive `implementsOf` implementations should pre-dedupe `owners`. ## #916 (resolveTypeRef) - Document the deliberate exclusion of `'Type'` from `TYPE_KINDS` (verified no extractor in `gitnexus/src/core/ingestion/` emits `type: 'Type'` for annotation-relevant symbols). - Rename the namespace-origin test from `'resolves ...'` to `'returns null for a namespace-origin binding whose def is not a type kind'`, matching the failure-case intent. ## #918 (shadow diff + aggregate) - Remove the partial re-export `export type { ShadowAgreement, ShadowDiff };` from `aggregate.ts` — it omitted `ShadowCallsite` and diverged from the top-level barrel. Consumers import all three from the `gitnexus-shared` entry point. - Fix the invalid `'wildcard'` evidence kind in `diff.test.ts` fixture (that kind is not a valid `ResolutionEvidence.kind`). Replaced with `'global-name'`, a real kind the test treats identically. ## #912 (ScopeTree / PositionIndex / makeScopeId) - Document the touching-boundary semantics on `PositionIndex.atPosition`: when siblings share a boundary point, the right (later-start) sibling wins per the existing innermost-wins sort contract. - Resolve the layer-inversion flagged by review: move `ScopeLookup` from `resolve-type-ref.ts` to `types.ts` (its natural home in the data-model layer). `scope-tree.ts` now imports `ScopeLookup` from `types.js` directly; the old re-export from `resolve-type-ref.ts` is removed per repo convention (`feedback_no_reexport`). Barrel export moved alongside. ## #917 (ClassRegistry / MethodRegistry / FieldRegistry) - Replace the dangling "try a name-match among class-like defs" comment in `lookupReceiverType` with explicit prose that callers must pre-resolve via `resolveTypeRef` if they want richer semantics. No behavior change — the function already returned `undefined` on ambiguous/missing qnames. - Fix `tieBreakKey.origin` default for pure Step-2 candidates. Type-binding-only hits no longer falsely inherit `'local'` from `ensureCandidate`'s neutral default; they now demote to `'import'` on their first type-binding hit, and only a later Step-1 lexical hit can upgrade them back to `'local'`. Keeps the Appendix B cascade faithful to the true origin. - Document `'global-name'` in `evidence.ts`: currently reserved for Ring 3's byName global index; `lookupCore` never emits it today. The weight stays live so `composeEvidence` remains exhaustive over the origin union. - Rename the mislabeled Step-7 test from `'confidence DESC is the primary key'` (which actually tested hard-shadow baseline) to `'inner scope shadows outer, yielding single result'`, and add a separate test that actually exercises multi-candidate confidence ordering (local vs wildcard at the same scope). ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - Combined scope-resolution / model / shadow suite: **260/260 pass** (+1 from the new multi-candidate ordering test in #917) ## Not addressed (non-actionable) - #949 CI "failure with zero failing tests": pre-existing Swift Node 22 grammar flake unrelated to #910 scope. - #950: the two non-blocking findings were already addressed in follow-up commit `cbac32ba` (ParsedImport discriminated union + `ScopeId | null` on the two hooks). - #915: the five in-scope findings were already addressed in follow-up commit `54515a7e` (dead code, unused params, multi-hop docs, cap-hit test, stats granularity). - #915 LanguageProvider.resolveImportTarget signature divergence + `findDefById` O(F×D) perf: tracked separately as follow-up issues for the Ring 3 migration window. * chore(shared): address ce:review findings on the follow-up diff ce:review (interactive) on PR #964 surfaced two P2s and several P3s. This commit applies all `safe_auto` fixes + both manual tests in-line so the PR ships with a cleaner review trail. ## P2 fixes - **Complete `freezeIndex` → `wrapIndex` rename.** The prior commit renamed 3 of 5 sibling index files; `method-dispatch-index.ts` and `position-index.ts` still carried the old name. Now all 5 helpers use the consistent `wrapIndex` naming. (maintainability + project-standards reviewers both flagged this.) - **Add regression tests for the `recordTypeBindingHit` origin demotion.** The prior commit introduced the `tieBreakKey.origin = 'import'` demotion for Step-2-only candidates without a direct test. Added: - `registries.test.ts`: two Step-2-only siblings under the same interface, asserting deterministic DefId.localeCompare tie-break AND the stronger invariant that composeEvidence never emits a where-found signal for Step-2-only candidates (no `signals.origin`). - `position-index.test.ts`: touching-boundary test proving the right-sibling-wins rule documented in the new JSDoc. (testing + kieran-typescript + api-contract reviewers all flagged these gaps.) ## P3 fixes - Fix wrong comment in `recordTypeBindingHit` that claimed Step 1 could later upgrade a demoted origin. Step 1 runs BEFORE Step 2 — the actual upgrade path is Step 3 (`seedFromOwnerScopedContributor`). Comment now describes execution order correctly. - Fix inaccurate "re-exported there" comment in `index.ts`. `types.ts` *defines* ScopeLookup natively; it's not a re-export. Phrasing now says "defined in types.ts and exported from the type-export block above — not from this module." - Update stale `scope-tree.ts` file-header prose that still referenced `ScopeLookup` as living in #916/resolve-type-ref.ts. Now points to `./types.js` with a cross-ref to both #916 and #917 consumers. - Expand `atPosition` touching-boundary JSDoc to name the mechanism (backward scan through start-sorted array) so readers can trace the binary-search code to the claim. - Add breadcrumb to `aggregate.ts` module header pointing future readers to `./diff.ts` / the top-level barrel for `ShadowAgreement`, `ShadowCallsite`, and `ShadowDiff`. - Remove unnecessary non-null assertion in `recordTypeBindingHit`. Local `const existingMroDepth = ...` lets TS narrow to `number` in the else-branch, eliminating the `!` without behavior change. ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - Combined scope-resolution / model / shadow suite: **262/262 pass** (+2 from the new origin-demotion + touching-boundary regression tests)
192 lines
7.5 KiB
TypeScript
192 lines
7.5 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']),
|
|
// The 2nd entry is here to verify index-0 isolation — the only kind
|
|
// requirement is that it be a valid `ResolutionEvidence.kind` so the
|
|
// fixture is type-correct. `'global-name'` is a real kind that
|
|
// `diffResolutions` never treats specially.
|
|
makeResolution('def:yet-another', ['global-name']),
|
|
];
|
|
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',
|
|
]);
|
|
});
|
|
});
|