diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index 7910dde60..769125ad5 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -415,6 +415,14 @@ export const resolvePdgConfig = (options: PdgOptions): RepoMeta['pdg'] => // outlive the model that produced them — ANY model-content change // ships as a new digest and repopulates the taint edges. taintModelVersion, + // #2201 review R3: reaching-defs solver identity. The SSA-sparse rewrite + // computes full facts for deep-loop functions the dense worklist used to + // truncate to empty, so an existing `--pdg` index carries stale-truncated + // REACHING_DEF rows. Absent on any pre-#2201 stamp → the key-union + // pdgModeMismatch trips on the first upgraded run and forces the full + // writeback that recomputes the fuller coverage (no `--force` needed). + // Bump this tag on any future change to which facts the solver emits. + reachingDefSolver: 'ssa-sparse-v1', } : undefined; diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index b03db9fc5..9e39a6cc0 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -194,6 +194,19 @@ export interface RepoMeta { * without `--force`. Optional: absent on pre-M3 stamps. */ taintModelVersion?: string; + /** + * Identity of the reaching-definitions solver the persisted REACHING_DEF + * rows were produced under (#2201 review R3). The SSA-sparse rewrite computes + * FULL facts for deep-loop functions the old dense worklist truncated to + * empty (the blocks×64 ceiling no longer fires) — but an existing `--pdg` + * index built under the old solver carries those truncated rows. ABSENT on + * any pre-#2201 stamp, so that absence trips `pdgModeMismatch` on the first + * upgraded run and forces the full writeback that recomputes the now-fuller + * REACHING_DEF coverage without `--force`. Bump the tag on any future change + * that alters which facts the solver emits. Optional for that upgrade reason; + * resolved (always present) on every post-#2201 write. + */ + reachingDefSolver?: string; }; } diff --git a/gitnexus/test/unit/pdg-mode-flip.test.ts b/gitnexus/test/unit/pdg-mode-flip.test.ts index 5e8dbe2f9..585747eb7 100644 --- a/gitnexus/test/unit/pdg-mode-flip.test.ts +++ b/gitnexus/test/unit/pdg-mode-flip.test.ts @@ -176,6 +176,42 @@ describe('pdgModeMismatch — pre-M5→M5 CDG-cap stamp upgrade (#2085 M5, pure) }); }); +describe('pdgModeMismatch — pre-#2201→SSA reaching-defs solver upgrade (#2201 review R3, pure)', () => { + it('resolvePdgConfig stamps the reaching-defs solver identity', async () => { + const { resolvePdgConfig } = await import('../../src/core/run-analyze.js'); + const stamp = resolvePdgConfig({ pdg: true }); + expect(stamp?.reachingDefSolver).toBe('ssa-sparse-v1'); + }); + + it('a pre-#2201 stamp (no solver key) mismatches the SSA request — upgrade recomputes truncated deep-loop facts', async () => { + const { pdgModeMismatch } = await import('../../src/core/run-analyze.js'); + // What a pre-#2201 (M5-era) run wrote: every cap + model digest, but NO + // reachingDefSolver. The key-union comparator sees 'ssa-sparse-v1' !== + // undefined and trips the full writeback that recomputes the now-fuller + // REACHING_DEF coverage — the deep-loop functions the dense worklist + // truncated to empty at the blocks×64 ceiling now compute full facts. + const m5Stamp = { + maxFunctionLines: 2000, + maxEdgesPerFunction: 5000, + maxReachingDefEdgesPerFunction: 4000, + maxCdgEdgesPerFunction: 5000, + maxTaintFindingsPerFunction: 200, + maxTaintHops: 32, + maxInterprocFindings: 2000, + maxInterprocHops: 32, + maxInterprocEdges: 1000, + taintModelVersion, + }; + expect(pdgModeMismatch(m5Stamp, { pdg: true })).toBe(true); + }); + + it('an identical post-#2201 stamp compares equal (no spurious re-analysis churn)', async () => { + const { pdgModeMismatch, resolvePdgConfig } = await import('../../src/core/run-analyze.js'); + const stamp = resolvePdgConfig({ pdg: true }); + expect(pdgModeMismatch(stamp, { pdg: true })).toBe(false); + }); +}); + describe('detect_changes BasicBlock exclusion (#2082 U7)', () => { it('the symbol-overlap id-prefix filter excludes exactly the BasicBlock rows', async () => { const repo = await setupMiniRepo(); @@ -258,6 +294,7 @@ describe('runFullAnalysis — pdg-mode flip (#2099 F1)', () => { maxInterprocHops: 32, maxInterprocEdges: 1000, taintModelVersion, + reachingDefSolver: 'ssa-sparse-v1', }); expect(stamped!.incrementalInProgress).toBeUndefined(); // cleared on success @@ -314,6 +351,7 @@ describe('runFullAnalysis — pdg-mode flip (#2099 F1)', () => { maxInterprocHops: 32, maxInterprocEdges: 1000, taintModelVersion, + reachingDefSolver: 'ssa-sparse-v1', }); // The CFG layer survives a rebuild under a tighter edge cap (blocks are // never capped, only edges). diff --git a/gitnexus/test/unit/run-analyze.test.ts b/gitnexus/test/unit/run-analyze.test.ts index d89114360..b317d6824 100644 --- a/gitnexus/test/unit/run-analyze.test.ts +++ b/gitnexus/test/unit/run-analyze.test.ts @@ -349,6 +349,10 @@ describe('pdgModeMismatch / resolvePdgConfig (#2099 F1)', () => { // Content digest, not a tunable cap — pinned via the exported constant // (its VALUE changes whenever the built-in model changes, by design). taintModelVersion, + // Solver identity, not a tunable cap — always stamped on a pdg-on run + // (#2201 review R3). Bumps when the reaching-defs solver's emitted facts + // change; absence on a pre-#2201 stamp forces a re-analysis. + reachingDefSolver: 'ssa-sparse-v1', }; it('resolvePdgConfig: pdg-off run resolves to undefined (the meta field is omitted)', async () => { @@ -384,6 +388,7 @@ describe('pdgModeMismatch / resolvePdgConfig (#2099 F1)', () => { maxInterprocHops: 0, maxInterprocEdges: 0, taintModelVersion, // not a cap — always stamped on a pdg-on run + reachingDefSolver: 'ssa-sparse-v1', // solver identity — always stamped (#2201 R3) }); });