mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
Ring 4 retires the legacy call-resolution DAG. With the legacy resolver gone (RING4-1 #942, RING4-2 #943), shadow mode has nothing to dual-run against, so the remaining shadow-mode artifacts are dead code. - Delete gitnexus-shared/src/scope-resolution/shadow/{diff,aggregate}.ts (pure parity comparison logic) and its gitnexus-shared barrel exports. - Delete the static parity dashboard (gitnexus/shadow-parity-dashboard/), which also removes the last GITNEXUS_SHADOW_MODE reference in the repo. - Delete the shadow-mode unit tests (gitnexus/test/unit/shadow/). - Scrub stale doc comments referencing the shadow harness / parity dashboard / removed legacy run (csharp/php/python/typescript index.ts, evidence.ts, module-scope-index.ts). Already removed by RING4-1/-2 (verified): the shadow harness source and GITNEXUS_SHADOW_MODE env handling; no CI job published dashboard artifacts. Historical parity records preserved per acceptance: the CHANGELOG entry (#918, #923, #951, #972) and the ci.yml RING4-1 note remain. Last documented parity state is that historical coverage — no live .gitnexus/shadow-parity/ run data exists in-tree (runtime output only). Closes #944. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0c292f9e7
commit
4fc2ffa5d0
12 changed files with 15 additions and 1062 deletions
|
|
@ -183,13 +183,3 @@ export {
|
|||
stripGitSuffix,
|
||||
} from './integrations/understand-quickly.js';
|
||||
export type { UqDispatchPayload } from './integrations/understand-quickly.js';
|
||||
|
||||
// Shadow-mode diff + aggregation (RFC §6.3; Ring 2 SHARED #918)
|
||||
export { diffResolutions } from './scope-resolution/shadow/diff.js';
|
||||
export type {
|
||||
ShadowAgreement,
|
||||
ShadowCallsite,
|
||||
ShadowDiff,
|
||||
} from './scope-resolution/shadow/diff.js';
|
||||
export { aggregateDiffs } from './scope-resolution/shadow/aggregate.js';
|
||||
export type { LanguageParityRow, ShadowParityReport } from './scope-resolution/shadow/aggregate.js';
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #913.
|
||||
*
|
||||
* Consumed by: #915 (SCC finalize link pass), #923 (shadow harness when
|
||||
* resolving callsite file → enclosing module).
|
||||
* Consumed by: #915 (SCC finalize link pass).
|
||||
*/
|
||||
|
||||
import type { ScopeId } from './types.js';
|
||||
|
|
|
|||
|
|
@ -57,8 +57,7 @@ export interface RawSignals {
|
|||
*
|
||||
* Emission order mirrors the `EvidenceWeights` layout: where-found →
|
||||
* type-binding → corroborators → arity → degraded. Stable order makes
|
||||
* the per-signal contributions easy to reason about in tests and in the
|
||||
* shadow-mode parity dashboard.
|
||||
* the per-signal contributions easy to reason about in tests.
|
||||
*/
|
||||
export function composeEvidence(signals: RawSignals): readonly ResolutionEvidence[] {
|
||||
const out: ResolutionEvidence[] = [];
|
||||
|
|
@ -141,7 +140,7 @@ export function composeEvidence(signals: RawSignals): readonly ResolutionEvidenc
|
|||
|
||||
/**
|
||||
* Sum evidence weights and clamp to `[0, 1]`. Separate from `composeEvidence`
|
||||
* so tests and the parity dashboard can inspect the raw evidence list.
|
||||
* so tests can inspect the raw evidence list.
|
||||
*/
|
||||
export function confidenceFromEvidence(evidence: readonly ResolutionEvidence[]): number {
|
||||
let sum = 0;
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
/**
|
||||
* Shadow-mode aggregation — per-language parity %, per-evidence-kind
|
||||
* breakdown of divergences. Consumed by the parity dashboard (RING2-PKG-5).
|
||||
*
|
||||
* Pure functions; no I/O. The harness persists per-run JSON; the dashboard
|
||||
* reads `.gitnexus/shadow-parity/latest.json` and renders.
|
||||
*
|
||||
* Related types — `ShadowAgreement`, `ShadowCallsite`, `ShadowDiff` — are
|
||||
* defined alongside `diffResolutions` in `./diff.ts` and re-exported
|
||||
* through the top-level `gitnexus-shared` barrel. Consumers import all
|
||||
* three from `gitnexus-shared`, not from this module.
|
||||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #918.
|
||||
*/
|
||||
|
||||
import type { SupportedLanguages } from '../../languages.js';
|
||||
import type { ResolutionEvidence } from '../types.js';
|
||||
import type { ShadowAgreement, ShadowDiff } from './diff.js';
|
||||
|
||||
// ─── Aggregated report shape ────────────────────────────────────────────────
|
||||
|
||||
export interface LanguageParityRow {
|
||||
readonly language: SupportedLanguages;
|
||||
readonly totalCalls: number;
|
||||
readonly bothAgree: number;
|
||||
readonly onlyLegacy: number;
|
||||
readonly onlyNew: number;
|
||||
readonly bothDisagree: number;
|
||||
readonly bothEmpty: number;
|
||||
/**
|
||||
* Fraction in [0, 1]. Numerator = `bothAgree`; denominator = "calls where
|
||||
* at least one side resolved" = `totalCalls - bothEmpty`.
|
||||
*
|
||||
* When the denominator is 0 (all calls for this language were
|
||||
* `both-empty`), returns 0. Callers rendering the dashboard should treat
|
||||
* a 0 parity alongside `totalCalls === bothEmpty` as "no signal" rather
|
||||
* than "total disagreement".
|
||||
*/
|
||||
readonly parity: number;
|
||||
/**
|
||||
* Divergence signals broken down by `ResolutionEvidence.kind`. Sourced
|
||||
* from `ShadowDiff.evidenceDelta` on non-agreeing rows only — `both-agree`
|
||||
* and `both-empty` do not contribute.
|
||||
*/
|
||||
readonly evidenceBreakdown: ReadonlyMap<ResolutionEvidence['kind'], number>;
|
||||
}
|
||||
|
||||
export interface ShadowParityReport {
|
||||
readonly generatedAt: string; // ISO 8601
|
||||
readonly perLanguage: readonly LanguageParityRow[];
|
||||
readonly overall: Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'>;
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aggregate a stream of `ShadowDiff` records into a `ShadowParityReport`,
|
||||
* bucketed by language. Pure function.
|
||||
*
|
||||
* - `perLanguage` rows are sorted alphabetically by `SupportedLanguages`
|
||||
* value for stable JSON output (the dashboard reads
|
||||
* `.gitnexus/shadow-parity/latest.json` and diffing snapshots is useful).
|
||||
* - `overall` is the column-wise sum across languages.
|
||||
* - `generatedAt` is injected via the `now` parameter so tests stay
|
||||
* deterministic; production callers let it default to `new Date()`.
|
||||
*/
|
||||
export function aggregateDiffs(
|
||||
diffs: readonly { readonly language: SupportedLanguages; readonly diff: ShadowDiff }[],
|
||||
now: Date = new Date(),
|
||||
): ShadowParityReport {
|
||||
const perLanguageMap = new Map<SupportedLanguages, MutableCounts>();
|
||||
|
||||
for (const { language, diff } of diffs) {
|
||||
let counts = perLanguageMap.get(language);
|
||||
if (!counts) {
|
||||
counts = makeEmptyCounts();
|
||||
perLanguageMap.set(language, counts);
|
||||
}
|
||||
tallyDiff(counts, diff);
|
||||
}
|
||||
|
||||
const perLanguage: LanguageParityRow[] = Array.from(perLanguageMap.entries())
|
||||
.map(([language, counts]) => buildRow(language, counts))
|
||||
.sort((a, b) => a.language.localeCompare(b.language));
|
||||
|
||||
const overall = buildOverallRow(perLanguage);
|
||||
|
||||
return {
|
||||
generatedAt: now.toISOString(),
|
||||
perLanguage,
|
||||
overall,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
interface MutableCounts {
|
||||
totalCalls: number;
|
||||
bothAgree: number;
|
||||
onlyLegacy: number;
|
||||
onlyNew: number;
|
||||
bothDisagree: number;
|
||||
bothEmpty: number;
|
||||
evidenceBreakdown: Map<ResolutionEvidence['kind'], number>;
|
||||
}
|
||||
|
||||
function makeEmptyCounts(): MutableCounts {
|
||||
return {
|
||||
totalCalls: 0,
|
||||
bothAgree: 0,
|
||||
onlyLegacy: 0,
|
||||
onlyNew: 0,
|
||||
bothDisagree: 0,
|
||||
bothEmpty: 0,
|
||||
evidenceBreakdown: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function tallyDiff(counts: MutableCounts, diff: ShadowDiff): void {
|
||||
counts.totalCalls += 1;
|
||||
incrementAgreement(counts, diff.agreement);
|
||||
if (diff.agreement === 'both-agree' || diff.agreement === 'both-empty') return;
|
||||
for (const ev of diff.evidenceDelta) {
|
||||
counts.evidenceBreakdown.set(ev.kind, (counts.evidenceBreakdown.get(ev.kind) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function incrementAgreement(counts: MutableCounts, agreement: ShadowAgreement): void {
|
||||
switch (agreement) {
|
||||
case 'both-agree':
|
||||
counts.bothAgree += 1;
|
||||
return;
|
||||
case 'only-legacy':
|
||||
counts.onlyLegacy += 1;
|
||||
return;
|
||||
case 'only-new':
|
||||
counts.onlyNew += 1;
|
||||
return;
|
||||
case 'both-disagree':
|
||||
counts.bothDisagree += 1;
|
||||
return;
|
||||
case 'both-empty':
|
||||
counts.bothEmpty += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRow(language: SupportedLanguages, counts: MutableCounts): LanguageParityRow {
|
||||
const resolved = counts.totalCalls - counts.bothEmpty;
|
||||
const parity = resolved > 0 ? counts.bothAgree / resolved : 0;
|
||||
return {
|
||||
language,
|
||||
totalCalls: counts.totalCalls,
|
||||
bothAgree: counts.bothAgree,
|
||||
onlyLegacy: counts.onlyLegacy,
|
||||
onlyNew: counts.onlyNew,
|
||||
bothDisagree: counts.bothDisagree,
|
||||
bothEmpty: counts.bothEmpty,
|
||||
parity,
|
||||
// Freeze via `new Map` on a sorted-kind copy so downstream consumers
|
||||
// can't mutate the aggregator's internal state.
|
||||
evidenceBreakdown: new Map(
|
||||
Array.from(counts.evidenceBreakdown.entries()).sort(([a], [b]) => a.localeCompare(b)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildOverallRow(
|
||||
perLanguage: readonly LanguageParityRow[],
|
||||
): Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'> {
|
||||
let totalCalls = 0;
|
||||
let bothAgree = 0;
|
||||
let onlyLegacy = 0;
|
||||
let onlyNew = 0;
|
||||
let bothDisagree = 0;
|
||||
let bothEmpty = 0;
|
||||
for (const row of perLanguage) {
|
||||
totalCalls += row.totalCalls;
|
||||
bothAgree += row.bothAgree;
|
||||
onlyLegacy += row.onlyLegacy;
|
||||
onlyNew += row.onlyNew;
|
||||
bothDisagree += row.bothDisagree;
|
||||
bothEmpty += row.bothEmpty;
|
||||
}
|
||||
const resolved = totalCalls - bothEmpty;
|
||||
const parity = resolved > 0 ? bothAgree / resolved : 0;
|
||||
return { totalCalls, bothAgree, onlyLegacy, onlyNew, bothDisagree, bothEmpty, parity };
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/**
|
||||
* Shadow-mode diff logic — RFC §6.3.
|
||||
*
|
||||
* Pure comparison logic for shadow mode. Takes two `Resolution[]` (legacy
|
||||
* DAG result + new scope-based registry result) and produces a structured
|
||||
* diff record for the parity dashboard.
|
||||
*
|
||||
* Consumed by the Ring 2 PKG shadow harness (#923), which dual-runs each
|
||||
* call through legacy + new paths, diffs results, and persists per-run JSON
|
||||
* for the parity dashboard.
|
||||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #918.
|
||||
*/
|
||||
|
||||
import type { Resolution, ResolutionEvidence } from '../types.js';
|
||||
|
||||
// ─── Diff record shape ──────────────────────────────────────────────────────
|
||||
|
||||
export type ShadowAgreement =
|
||||
| 'both-agree' // top match identical (same DefId)
|
||||
| 'only-legacy' // legacy resolved; new did not
|
||||
| 'only-new' // new resolved; legacy did not
|
||||
| 'both-disagree' // both resolved, but to different targets
|
||||
| 'both-empty'; // both returned empty
|
||||
|
||||
export interface ShadowDiff {
|
||||
readonly callsite: ShadowCallsite;
|
||||
readonly legacy: Resolution | null;
|
||||
readonly newResult: Resolution | null;
|
||||
readonly agreement: ShadowAgreement;
|
||||
/**
|
||||
* Symmetric difference of the two top resolutions' `evidence` arrays,
|
||||
* keyed on `ResolutionEvidence.kind`.
|
||||
*
|
||||
* - For `'both-agree'` and `'both-empty'` agreements, always empty.
|
||||
* - For `'both-disagree'`, contains evidence kinds present on exactly one
|
||||
* side (not in both).
|
||||
* - For `'only-legacy'`, contains all of legacy's top evidence.
|
||||
* - For `'only-new'`, contains all of new's top evidence.
|
||||
*/
|
||||
readonly evidenceDelta: readonly ResolutionEvidence[];
|
||||
}
|
||||
|
||||
export interface ShadowCallsite {
|
||||
readonly filePath: string;
|
||||
readonly line: number;
|
||||
readonly col: number;
|
||||
readonly calledName: string;
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compare two `Resolution[]` arrays (top matches at `[0]`) and produce a
|
||||
* `ShadowDiff`. Pure function.
|
||||
*
|
||||
* Agreement rules:
|
||||
* - both arrays empty → `'both-empty'`, `evidenceDelta: []`
|
||||
* - legacy empty, new non-empty → `'only-new'`, `evidenceDelta` = new's top evidence
|
||||
* - legacy non-empty, new empty → `'only-legacy'`, `evidenceDelta` = legacy's top evidence
|
||||
* - both non-empty, same top `def.nodeId` → `'both-agree'`, `evidenceDelta: []`
|
||||
* - both non-empty, different top `def.nodeId` → `'both-disagree'`,
|
||||
* `evidenceDelta` = symmetric difference by `ResolutionEvidence.kind`
|
||||
* (first occurrence of a kind-only-on-legacy then kind-only-on-new; order
|
||||
* preserved from input arrays)
|
||||
*
|
||||
* Evidence-delta rationale: callers aggregating divergences want to know
|
||||
* which signal kinds explain a disagreement. Keying on `kind` (not full
|
||||
* equality over `weight`/`note`) avoids spurious deltas when the same
|
||||
* signal fires with slightly different calibration weights on each side.
|
||||
*/
|
||||
export function diffResolutions(
|
||||
callsite: ShadowCallsite,
|
||||
legacy: readonly Resolution[],
|
||||
newResult: readonly Resolution[],
|
||||
): ShadowDiff {
|
||||
const legacyTop: Resolution | null = legacy.length > 0 ? legacy[0] : null;
|
||||
const newTop: Resolution | null = newResult.length > 0 ? newResult[0] : null;
|
||||
|
||||
const agreement: ShadowAgreement = (() => {
|
||||
if (legacyTop === null && newTop === null) return 'both-empty';
|
||||
if (legacyTop === null) return 'only-new';
|
||||
if (newTop === null) return 'only-legacy';
|
||||
return legacyTop.def.nodeId === newTop.def.nodeId ? 'both-agree' : 'both-disagree';
|
||||
})();
|
||||
|
||||
const evidenceDelta = computeEvidenceDelta(legacyTop, newTop, agreement);
|
||||
|
||||
return {
|
||||
callsite,
|
||||
legacy: legacyTop,
|
||||
newResult: newTop,
|
||||
agreement,
|
||||
evidenceDelta,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Symmetric difference of two evidence arrays, keyed on
|
||||
* `ResolutionEvidence.kind`. Preserves input order: legacy-only signals
|
||||
* first (in legacy's original order), then new-only signals (in new's order).
|
||||
*
|
||||
* For `'both-agree'` / `'both-empty'` the delta is empty by contract. For
|
||||
* `'only-legacy'` / `'only-new'` one side's evidence is the delta (nothing to
|
||||
* subtract against).
|
||||
*/
|
||||
function computeEvidenceDelta(
|
||||
legacy: Resolution | null,
|
||||
newResult: Resolution | null,
|
||||
agreement: ShadowAgreement,
|
||||
): readonly ResolutionEvidence[] {
|
||||
if (agreement === 'both-agree' || agreement === 'both-empty') return [];
|
||||
if (agreement === 'only-legacy') return legacy!.evidence;
|
||||
if (agreement === 'only-new') return newResult!.evidence;
|
||||
|
||||
// both-disagree: symmetric difference keyed on `kind`
|
||||
const legacyKinds = new Set(legacy!.evidence.map((e) => e.kind));
|
||||
const newKinds = new Set(newResult!.evidence.map((e) => e.kind));
|
||||
|
||||
const onlyInLegacy = legacy!.evidence.filter((e) => !newKinds.has(e.kind));
|
||||
const onlyInNew = newResult!.evidence.filter((e) => !legacyKinds.has(e.kind));
|
||||
|
||||
return [...onlyInLegacy, ...onlyInNew];
|
||||
}
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>GitNexus — Shadow Parity Dashboard</title>
|
||||
<!--
|
||||
Static dashboard for the RFC #909 shadow-mode parity report.
|
||||
|
||||
Reads `latest.json` from this directory and renders a per-language
|
||||
parity table. Zero build step, zero runtime dependencies — a
|
||||
single file that any browser or file:// context can open.
|
||||
|
||||
Usage:
|
||||
# from repo root, after a shadow-mode run
|
||||
cp .gitnexus/shadow-parity/latest.json gitnexus/shadow-parity-dashboard/
|
||||
open gitnexus/shadow-parity-dashboard/index.html
|
||||
|
||||
CI artifact wiring (follow-up): the CI job publishes a snapshot
|
||||
of this directory + latest.json as a downloadable bundle per run.
|
||||
-->
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--fg: #1f2937;
|
||||
--fg-muted: #6b7280;
|
||||
--bg: #ffffff;
|
||||
--bg-muted: #f9fafb;
|
||||
--border: #e5e7eb;
|
||||
--good: #16a34a;
|
||||
--warn: #d97706;
|
||||
--bad: #dc2626;
|
||||
--primary-tag-legacy: #7c3aed;
|
||||
--primary-tag-registry: #0ea5e9;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fg: #e5e7eb;
|
||||
--fg-muted: #9ca3af;
|
||||
--bg: #111827;
|
||||
--bg-muted: #1f2937;
|
||||
--border: #374151;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font:
|
||||
14px/1.45 system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
}
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.meta {
|
||||
color: var(--fg-muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
.card .k {
|
||||
color: var(--fg-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.card .v {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
thead th {
|
||||
font-weight: 600;
|
||||
color: var(--fg-muted);
|
||||
font-size: 12px;
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
.parity {
|
||||
font-weight: 600;
|
||||
}
|
||||
.parity.good {
|
||||
color: var(--good);
|
||||
}
|
||||
.parity.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
.parity.bad {
|
||||
color: var(--bad);
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
margin-left: 6px;
|
||||
color: white;
|
||||
}
|
||||
.tag.legacy {
|
||||
background: var(--primary-tag-legacy);
|
||||
}
|
||||
.tag.registry {
|
||||
background: var(--primary-tag-registry);
|
||||
}
|
||||
.empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
background: var(--bg-muted);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Shadow Parity — RFC #909</h1>
|
||||
<div class="meta" id="meta">loading <code>latest.json</code>…</div>
|
||||
<div class="cards" id="cards"></div>
|
||||
<table id="per-language">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Language</th>
|
||||
<th>Total</th>
|
||||
<th>Agree</th>
|
||||
<th>Only legacy</th>
|
||||
<th>Only new</th>
|
||||
<th>Disagree</th>
|
||||
<th>Both empty</th>
|
||||
<th>Parity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div id="empty" class="empty" style="display: none">
|
||||
No records yet. Enable <code>GITNEXUS_SHADOW_MODE=1</code> and run ingestion to populate.
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
/* global fetch, document */
|
||||
(async function () {
|
||||
const tbody = document.querySelector('#per-language tbody');
|
||||
const cards = document.getElementById('cards');
|
||||
const meta = document.getElementById('meta');
|
||||
const empty = document.getElementById('empty');
|
||||
const table = document.getElementById('per-language');
|
||||
|
||||
let payload;
|
||||
try {
|
||||
const r = await fetch('./latest.json', { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
payload = await r.json();
|
||||
} catch (err) {
|
||||
meta.textContent = 'Failed to load latest.json: ' + err.message;
|
||||
table.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const primary = payload.primaryByLanguage || {};
|
||||
const report = payload.report || {};
|
||||
const perLang = report.perLanguage || [];
|
||||
const overall = report.overall || {};
|
||||
|
||||
meta.textContent =
|
||||
'Run ' +
|
||||
payload.runId +
|
||||
' — generated ' +
|
||||
payload.generatedAt +
|
||||
' (schema v' +
|
||||
payload.schemaVersion +
|
||||
')';
|
||||
|
||||
// Overall summary cards.
|
||||
cards.innerHTML = '';
|
||||
const overallParity = overall.parity !== undefined ? overall.parity : 0;
|
||||
cards.appendChild(makeCard('Total calls', overall.totalCalls ?? 0));
|
||||
cards.appendChild(makeCard('Both agree', overall.bothAgree ?? 0));
|
||||
cards.appendChild(makeCard('Disagree', overall.bothDisagree ?? 0));
|
||||
cards.appendChild(makeCard('Overall parity', formatPct(overallParity)));
|
||||
|
||||
if (!perLang.length) {
|
||||
table.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of perLang) {
|
||||
const tr = document.createElement('tr');
|
||||
const primaryTag = primary[row.language];
|
||||
const tag = primaryTag
|
||||
? '<span class="tag ' + primaryTag + '">primary: ' + primaryTag + '</span>'
|
||||
: '';
|
||||
const parityClass = parityClassFor(row.parity);
|
||||
tr.innerHTML =
|
||||
'<td>' +
|
||||
escape(row.language) +
|
||||
tag +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.totalCalls +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothAgree +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.onlyLegacy +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.onlyNew +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothDisagree +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothEmpty +
|
||||
'</td>' +
|
||||
'<td class="parity ' +
|
||||
parityClass +
|
||||
'">' +
|
||||
formatPct(row.parity) +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function makeCard(k, v) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'card';
|
||||
div.innerHTML =
|
||||
'<div class="k">' + escape(k) + '</div><div class="v">' + escape(String(v)) + '</div>';
|
||||
return div;
|
||||
}
|
||||
function formatPct(x) {
|
||||
if (typeof x !== 'number' || !isFinite(x)) return '—';
|
||||
return (x * 100).toFixed(1) + '%';
|
||||
}
|
||||
function parityClassFor(x) {
|
||||
if (typeof x !== 'number') return '';
|
||||
if (x >= 0.95) return 'good';
|
||||
if (x >= 0.8) return 'warn';
|
||||
return 'bad';
|
||||
}
|
||||
function escape(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -68,10 +68,9 @@
|
|||
* `using static X = Y.Z;`, attributes, and preprocessor-gated
|
||||
* declarations are all recognized correctly.
|
||||
*
|
||||
* Shadow-harness corpus parity is the authoritative signal for which
|
||||
* of these matter in practice. The CI parity gate blocks any PR that
|
||||
* regresses either the legacy or registry-primary run of
|
||||
* `test/integration/resolvers/csharp.test.ts`.
|
||||
* The `test/integration/resolvers/csharp.test.ts` resolver suite is the
|
||||
* authoritative signal for which of these matter in practice; it runs in
|
||||
* the standard CI test workflow, so a regression blocks the merge.
|
||||
*/
|
||||
|
||||
export { emitCsharpScopeCaptures } from './captures.js';
|
||||
|
|
|
|||
|
|
@ -54,10 +54,9 @@
|
|||
* 6. **Intersection types in parameters** — `T&U $param` takes the first
|
||||
* named part (`T`). This matches the legacy type-extractor's behavior.
|
||||
*
|
||||
* Shadow-harness corpus parity is the authoritative signal for which of
|
||||
* these matter in practice. The CI parity gate blocks any PR that regresses
|
||||
* either the legacy or registry-primary run of
|
||||
* `test/integration/resolvers/php.test.ts`.
|
||||
* The `test/integration/resolvers/php.test.ts` resolver suite is the
|
||||
* authoritative signal for which of these matter in practice; it runs in
|
||||
* the standard CI test workflow, so a regression blocks the merge.
|
||||
*/
|
||||
|
||||
export { emitPhpScopeCaptures } from './captures.js';
|
||||
|
|
|
|||
|
|
@ -66,10 +66,9 @@
|
|||
* site where the enclosing class can't be statically determined
|
||||
* is left unresolved.
|
||||
*
|
||||
* Shadow-harness corpus parity is the authoritative signal for which
|
||||
* of these matter in practice. The CI parity gate blocks any PR that
|
||||
* regresses either the legacy or registry-primary run of
|
||||
* `test/integration/resolvers/python.test.ts`.
|
||||
* The `test/integration/resolvers/python.test.ts` resolver suite is the
|
||||
* authoritative signal for which of these matter in practice; it runs in
|
||||
* the standard CI test workflow, so a regression blocks the merge.
|
||||
*/
|
||||
|
||||
export { emitPythonScopeCaptures } from './captures.js';
|
||||
|
|
|
|||
|
|
@ -80,10 +80,9 @@
|
|||
* identifiers are narrowed (`user instanceof User`). Member paths
|
||||
* such as `user.address instanceof Address` remain unresolved.
|
||||
*
|
||||
* Shadow-harness corpus parity on `test/integration/resolvers/
|
||||
* typescript.test.ts` is the authoritative signal for which of these
|
||||
* matter in practice. The CI parity gate blocks any PR that regresses
|
||||
* either the legacy or registry-primary run.
|
||||
* The `test/integration/resolvers/typescript.test.ts` resolver suite is
|
||||
* the authoritative signal for which of these matter in practice; it runs
|
||||
* in the standard CI test workflow, so a regression blocks the merge.
|
||||
*/
|
||||
|
||||
export { emitTsScopeCaptures } from './captures.js';
|
||||
|
|
|
|||
|
|
@ -1,234 +0,0 @@
|
|||
/**
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
/**
|
||||
* 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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue