feat(pdg): stamp the reaching-defs solver identity for incremental re-analysis (#2201 review R3)

The SSA-sparse rewrite computes full REACHING_DEF facts for deep-loop functions
the old dense worklist truncated to empty at the blocks×64 ceiling. But an
existing `--pdg` index carries those stale-truncated rows, and nothing forced a
re-analysis: RepoMeta.pdg had no solver-identity key, so an upgraded run over an
unchanged file kept the incremental fast path and never recomputed.

Add a constant `reachingDefSolver: 'ssa-sparse-v1'` to the resolved pdg stamp
(and to the RepoMeta['pdg'] type). It rides the existing key-union
pdgModeMismatch comparator: a pre-#2201 stamp lacks the key, so
'ssa-sparse-v1' !== undefined trips one full writeback that recomputes the
fuller coverage — no `--force` needed — exactly like the M2 REACHING_DEF cap and
M5 CDG cap upgrade paths. A matching post-#2201 stamp compares equal, so there
is no spurious re-analysis churn on steady-state re-runs.

Tests: new pre-#2201→SSA upgrade block in pdg-mode-flip.test.ts (stamp present,
absent-key mismatch, identical-stamp no-churn) + the persisted-stamp shape
assertions and resolvePdgConfig DEFAULTS updated for the new key. tsc clean;
pdg-mode-flip + run-analyze suites green (55/55).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-15 17:21:43 +00:00
parent 20b44b0d65
commit 4dfafee20c
4 changed files with 64 additions and 0 deletions

View file

@ -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;

View file

@ -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;
};
}

View file

@ -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).

View file

@ -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)
});
});