From 6e6c300d48fbd1a0df8435ffcc90b7c76ac7e619 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 16 Jun 2026 08:41:19 +0000 Subject: [PATCH] test(impact-pdg): curated ground-truth fixture corpus (U6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 13-case corpus (12 measurable: intra=6/inter=3/mixed=3, + 1 excluded no-body) under bench/impact-pdg/fixtures. Each case: tiny TS source + ground-truth.json {criterion, intra_AIS, inter_AIS, locus, provenance, analyzerVersion, rationale}. Annotated from SOURCE SEMANTICS only (KTD9 annotation-circularity guard — U7 reconciles vs the traversal). Symbol/line granularity, not block-id. Schema+smoke test asserts each measurable criterion emits its declared PDG edges (catches accidental zero-edge/no-body criteria). Refs U6 --- gitnexus/bench/impact-pdg/README.md | 104 +++++ .../inter-dispatcher-thin/ground-truth.json | 35 ++ .../inter-dispatcher-thin/src/dispatcher.ts | 30 ++ .../inter-facade-delegate/ground-truth.json | 35 ++ .../inter-facade-delegate/src/facade.ts | 26 ++ .../inter-pipeline-stages/ground-truth.json | 35 ++ .../inter-pipeline-stages/src/pipeline.ts | 28 ++ .../intra-control-branch/ground-truth.json | 38 ++ .../intra-control-branch/src/branch.ts | 14 + .../intra-control-guard/ground-truth.json | 44 +++ .../fixtures/intra-control-guard/src/guard.ts | 14 + .../intra-control-loop/ground-truth.json | 32 ++ .../fixtures/intra-control-loop/src/loop.ts | 15 + .../ground-truth.json | 32 ++ .../src/accumulator.ts | 13 + .../intra-dataflow-chain/ground-truth.json | 20 + .../intra-dataflow-chain/src/chain.ts | 11 + .../intra-dataflow-reassign/ground-truth.json | 19 + .../intra-dataflow-reassign/src/reassign.ts | 10 + .../mixed-compute-and-emit/ground-truth.json | 44 +++ .../mixed-compute-and-emit/src/mixed.ts | 18 + .../mixed-guarded-dispatch/ground-truth.json | 49 +++ .../mixed-guarded-dispatch/src/mixed.ts | 21 ++ .../ground-truth.json | 50 +++ .../mixed-validate-then-call/src/mixed.ts | 20 + .../ground-truth.json | 15 + .../nobody-interface-excluded/src/nobody.ts | 16 + .../integration/impact-pdg-fixtures.test.ts | 356 ++++++++++++++++++ 28 files changed, 1144 insertions(+) create mode 100644 gitnexus/bench/impact-pdg/README.md create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/src/dispatcher.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/src/facade.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/src/pipeline.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-branch/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-branch/src/branch.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-guard/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-guard/src/guard.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-loop/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-control-loop/src/loop.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/src/accumulator.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/src/chain.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/src/reassign.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/src/mixed.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/src/mixed.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/src/mixed.ts create mode 100644 gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/ground-truth.json create mode 100644 gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/src/nobody.ts create mode 100644 gitnexus/test/integration/impact-pdg-fixtures.test.ts diff --git a/gitnexus/bench/impact-pdg/README.md b/gitnexus/bench/impact-pdg/README.md new file mode 100644 index 000000000..656af0a42 --- /dev/null +++ b/gitnexus/bench/impact-pdg/README.md @@ -0,0 +1,104 @@ +# `bench/impact-pdg` — PDG-vs-call-graph impact accuracy harness + +> **STATUS: STUB (U6).** This directory currently holds only the **curated +> ground-truth fixture corpus** (U6). The measurement harness (`measure.mjs`, +> `baselines.json`) and the full methodology write-up land in **U7**. This +> README documents the annotation schema and the validity posture so the +> fixtures are reviewable on their own. + +## What this measures + +`impact` has two engines: `mode: 'callgraph'` (the default — inter-procedural +BFS over symbol→symbol edges, over-approximating) and `mode: 'pdg'` (opt-in — +intra-procedural blast radius from the persisted CDG + REACHING_DEF Program +Dependence Graph). They occupy **different points on the precision/recall +curve**; neither strictly dominates. The U7 harness runs both over the fixtures +here and reports **precision / recall / F1 stratified by impact locus** so the +"which is more accurate?" question gets an honest, per-scope answer rather than +a single blended number. + +## The corpus + +Each case is a tiny self-contained TypeScript source repo plus a +`ground-truth.json`. TypeScript is used throughout because it has the most +mature CFG/PDG support in this codebase. + +| Case | Locus | Shape | +|---|---|---| +| `intra-dataflow-accumulator` | intra | loop-carried accumulator def→use (downstream) | +| `intra-dataflow-chain` | intra | straight-line def→use chain (downstream) | +| `intra-dataflow-reassign` | intra | reaching defs of a use (upstream, RD-reverse) | +| `intra-control-guard` | intra | guard-clause control dependence (downstream, CDG-forward) | +| `intra-control-branch` | intra | if/else-if/else arm control dependence (downstream) | +| `intra-control-loop` | intra | nested loop+if controllers of a stmt (upstream, CDG-reverse) | +| `inter-dispatcher-thin` | inter | branch router → 3 handlers (PDG ≈ ∅ by design) | +| `inter-facade-delegate` | inter | guarded sequential delegation chain | +| `inter-pipeline-stages` | inter | straight pipeline driver (upstream) | +| `mixed-validate-then-call` | mixed | guard-dominated intra dependence + 1 callee | +| `mixed-compute-and-emit` | mixed | data-flow-dominated intra dependence + 1 callee | +| `mixed-guarded-dispatch` | mixed | control+data intra dependence + 2 callees | +| `nobody-interface-excluded` | n/a | no-body symbols (KTD6); **excluded** from PDG scoring | + +**Minimum corpus floor (KTD9/F3):** ≥ 3 cases per locus stratum, ≥ 12 total +measurable cases. Current: intra = 6, inter = 3, mixed = 3 → 12 measurable +(+1 excluded no-body case). Below this floor the U7 harness must print +"underpowered — directional only" instead of a verdict. + +## Annotation schema (`ground-truth.json`) + +| Field | Type | Meaning | +|---|---|---| +| `schemaVersion` | int | schema version (currently `1`) | +| `criterion` | `{ name, filePath, direction, marker?, pdgEdgeKinds? }` | the changed symbol — the seed for "what is affected if I change this". `direction` ∈ `downstream` \| `upstream`. `marker` is a substring unique to the criterion function's body (appears in one of its `BasicBlock.text` fragments); the smoke test uses it to locate the criterion function's blocks deterministically. `pdgEdgeKinds` lists the PDG edge kinds (`REACHING_DEF` \| `CDG`) the criterion function is expected to produce: a pure straight-line data-flow criterion declares only `REACHING_DEF` (no branches → no control dependence), a branching/guard criterion declares both. The smoke test asserts exactly the declared kinds are non-zero on the criterion (so the pure-dataflow archetype isn't forced to carry an artificial branch) and that the criterion produces ≥ 1 PDG edge overall (catching an accidental no-body/zero-edge criterion). `marker` and `pdgEdgeKinds` are required for every measurable case; both are omitted only on `pdgScoring: "exclude"` no-body cases. | +| `intra_AIS` | `AisEntry[]` | symbols/**lines** truly affected WITHIN the same function (the scope where PDG mode is defined). Annotated at **symbol/line granularity, never block-id** (block ids carry fragile `fnLine:fnCol:idx`). | +| `inter_AIS` | `AisEntry[]` | symbols truly affected ACROSS function boundaries (the scope where call-graph mode is defined and intra-procedural PDG is zero-by-design). | +| `locus` | `'intra' \| 'inter' \| 'mixed' \| 'n/a'` | the dominant impact locus; `n/a` only for excluded no-body cases. | +| `pdgScoring` | `'exclude'` (optional) | present (= `"exclude"`) only on no-body cases U7 must drop from PDG denominators. | +| `provenance` | `'manual' \| 'mutation'` | how the AIS was derived. **v1 is `manual` only** — the mutation track (perturb a statement, diff the changed outcomes) needs a fixture-runner + value-diff harness that does not exist yet, so it is deferred. The field stays for forward-compatibility. | +| `analyzerVersion` | string | pinned analyzer version marker (currently the `package.json` version) so ground truth versions against the analyzer. | +| `rationale` | string | prose — WHY each AIS element is in or out. This is what makes manual annotation defensible (SLICEBENCH generate-then-verify discipline). | + +`AisEntry` = `{ symbol, filePath, line?, note? }`. `line` is 1-based and present +for intra entries (which are statement-granular); inter entries name a whole +symbol and omit `line`. + +`intra_AIS` and `inter_AIS` are **disjoint** for every case (an intra entry is a +line within the criterion function; an inter entry is a different symbol). + +## Validity threats (the two that dominate — KTD9) + +1. **Ground-truth incompleteness.** A hand-annotated handful of fixtures yields + *point estimates* over a tiny, self-admittedly incomplete corpus. One + mis-annotation can swing F1 by a large fraction, so U7 reports findings as a + **direction**, not a headline decimal, until the corpus grows / the mutation + track lands. +2. **Annotation circularity.** PDG's `intra_AIS` risks being reconciled against + the PDG traversal's own output. **Mitigation (KTD9 annotation-circularity + guard): these annotations are written from SOURCE SEMANTICS first** — reading + the source and reasoning about def→use / control dependence by hand — and + reconciling against the live traversal is **U7's job (its Step 0), not the + annotation's**. Call-graph gets no such home-field annotation, so the + comparison is not rigged toward PDG. + +## Annotation fingerprint (KTD10) + +U7 computes an **order-independent fingerprint over this annotation set** so an +*unreviewed edit to ground truth* — which silently moves the metric — trips a +distinct `--check` gate (separate from the one-sided F1 regression band). The +canonicalizer is annotation-set-shaped (it mirrors the `bench/cfg/measure.mjs` +technique, not a literal import). + +## How to run + +`measure.mjs` is **not yet built** (U7). When it lands: + +``` +node --import tsx gitnexus/bench/impact-pdg/measure.mjs # print the stratified report +node --import tsx gitnexus/bench/impact-pdg/measure.mjs --check # gate against baselines.json +``` + +The fixtures are validated today by the integration test +`test/integration/impact-pdg-fixtures.test.ts` (schema well-formedness + a smoke +test that each fixture analyzes under `--pdg` and the criterion function +produces CDG + REACHING_DEF edges — a zero-edge criterion has unmeasurable +ground truth). diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/ground-truth.json new file mode 100644 index 000000000..86e7387cd --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/ground-truth.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "dispatch", + "filePath": "src/dispatcher.ts", + "direction": "downstream", + "marker": "kind === 'b'", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "inter", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [], + "inter_AIS": [ + { + "symbol": "handleA", + "filePath": "src/dispatcher.ts", + "note": "routed to when kind === 'a'; its behavior is part of dispatch's true blast radius" + }, + { + "symbol": "handleB", + "filePath": "src/dispatcher.ts", + "note": "routed to when kind === 'b'" + }, + { + "symbol": "handleDefault", + "filePath": "src/dispatcher.ts", + "note": "the fallthrough route" + } + ], + "rationale": "`dispatch` is a thin router whose true impact is ENTIRELY cross-function: changing it affects the three handlers it dispatches to (handleA/handleB/handleDefault). There is no meaningful intra-procedural blast radius — the only intra-statements are the routing branch returns, which carry no work of their own. So intra_AIS is empty and inter_AIS is the three callees. PDG mode is intra-procedural by design, so on this case its inter-AIS recall is ~0 BY DESIGN (a capability fact, not a defect); the call-graph mode, with inter-procedural reach, finds all three. This case anchors the `inter` stratum so the harness can report complementary coverage. NOTE: the smoke test still requires `dispatch` to produce SOME CDG/REACHING_DEF edges (its routing branches do) — a zero-edge criterion would be unmeasurable; but those edges are not the AIS." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/src/dispatcher.ts b/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/src/dispatcher.ts new file mode 100644 index 000000000..1b7af560b --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-dispatcher-thin/src/dispatcher.ts @@ -0,0 +1,30 @@ +// Thin cross-function dispatcher. `dispatch`'s TRUE impact is entirely +// cross-function: it just routes to `handleA` / `handleB` / `handleDefault`. +// Intra-procedural PDG over `dispatch` returns ~no truly-affected statements +// of interest (the routing branch returns are control-dependent on the +// selector, but the *meaningful* blast radius — the work — lives in the +// callees), so PDG inter-AIS recall here is ~0 BY DESIGN. The call-graph mode +// is the right tool: its inter-procedural reach finds the three handlers. + +export function handleA(payload: number): number { + return payload + 1; +} + +export function handleB(payload: number): number { + return payload * 2; +} + +export function handleDefault(payload: number): number { + return payload; +} + +export function dispatch(kind: string, payload: number): number { + // criterion: a thin router. Changing it affects the callees it routes to. + if (kind === 'a') { + return handleA(payload); + } + if (kind === 'b') { + return handleB(payload); + } + return handleDefault(payload); +} diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/ground-truth.json new file mode 100644 index 000000000..3db73da58 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/ground-truth.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "processOrder", + "filePath": "src/facade.ts", + "direction": "downstream", + "marker": "enrich(order)", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "inter", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [], + "inter_AIS": [ + { + "symbol": "validate", + "filePath": "src/facade.ts", + "note": "the guard delegate; processOrder's behavior depends on it" + }, + { + "symbol": "enrich", + "filePath": "src/facade.ts", + "note": "transforms the order before persistence" + }, + { + "symbol": "persist", + "filePath": "src/facade.ts", + "note": "the terminal delegate in the sequence" + } + ], + "rationale": "`processOrder` is a facade sequencing three delegates (validate, enrich, persist) with one guard. Its true blast radius is the delegation chain — the inter_AIS — not its own body, which only routes values between calls. intra_AIS is empty: the guard return and the local `enriched` binding carry no independent work beyond shuttling delegate results. PDG mode (intra-procedural) has ~0 inter-AIS recall here BY DESIGN; call-graph mode walks validate->enrich->persist. Anchors the `inter` stratum with a sequential-delegation shape (distinct from the dispatcher's branching shape). The single guard gives `processOrder` a CDG edge so the smoke test's edge-presence requirement is met." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/src/facade.ts b/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/src/facade.ts new file mode 100644 index 000000000..a4c93b487 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-facade-delegate/src/facade.ts @@ -0,0 +1,26 @@ +// Facade that delegates to a layered set of helpers. `processOrder`'s true +// impact is cross-function: it sequences `validate`, `enrich`, and `persist`. +// Its own body is a thin sequence of calls with a single guard; the real work +// (and the real blast radius) is in the delegates. PDG intra-AIS is ~empty by +// design; the call-graph mode walks the delegation chain. + +export function validate(order: number): boolean { + return order > 0; +} + +export function enrich(order: number): number { + return order + 100; +} + +export function persist(order: number): number { + return order; +} + +export function processOrder(order: number): number { + // criterion: a facade. Its impact flows into the three delegates. + if (!validate(order)) { + return -1; + } + const enriched = enrich(order); + return persist(enriched); +} diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/ground-truth.json new file mode 100644 index 000000000..67733f399 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/ground-truth.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "runPipeline", + "filePath": "src/pipeline.ts", + "direction": "upstream", + "marker": "stageTransform(acc)", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "inter", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [], + "inter_AIS": [ + { + "symbol": "stageParse", + "filePath": "src/pipeline.ts", + "note": "first pipeline stage runPipeline invokes; its output feeds the next" + }, + { + "symbol": "stageTransform", + "filePath": "src/pipeline.ts", + "note": "second stage; depends on stageParse's output threaded through `acc`" + }, + { + "symbol": "stageEmit", + "filePath": "src/pipeline.ts", + "note": "terminal stage" + } + ], + "rationale": "Direction is UPSTREAM: asking what `runPipeline` depends on. As a thin driver it threads `acc` through three stage calls (stageParse->stageTransform->stageEmit); the dependencies that matter cross function boundaries — the three stage functions — so inter_AIS holds them. intra_AIS is empty: the `acc` reassignments only carry delegate results, no independent computation. PDG mode is intra-procedural, so its inter-AIS recall here is ~0 BY DESIGN; the call-graph mode (upstream) reaches the stages. The `enabled` guard (added so the driver carries a CDG edge) keeps the criterion measurable for the smoke test without changing the cross-function locus. Distinct from the dispatcher (branch-routed) and facade (guarded-sequence) shapes — this is a straight pipeline driver." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/src/pipeline.ts b/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/src/pipeline.ts new file mode 100644 index 000000000..06401c62e --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/inter-pipeline-stages/src/pipeline.ts @@ -0,0 +1,28 @@ +// A pipeline driver whose impact is cross-function: `runPipeline` calls each +// stage in turn. The stages themselves carry the work; the driver loops over +// them. Annotated UPSTREAM from the driver: "what does runPipeline depend on?" +// -> the stage functions it invokes. Intra-PDG is ~empty by design. + +export function stageParse(n: number): number { + return n + 1; +} + +export function stageTransform(n: number): number { + return n * 3; +} + +export function stageEmit(n: number): number { + return n - 2; +} + +export function runPipeline(seed: number, enabled: boolean): number { + // criterion (upstream): a driver. It depends on the three stage functions. + let acc = seed; + if (!enabled) { + return acc; // a guard so the driver itself carries a CDG edge + } + acc = stageParse(acc); + acc = stageTransform(acc); + acc = stageEmit(acc); + return acc; +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/ground-truth.json new file mode 100644 index 000000000..09f964f51 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/ground-truth.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "classify", + "filePath": "src/branch.ts", + "direction": "downstream", + "marker": "'zero'", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "classify", + "filePath": "src/branch.ts", + "line": 9, + "note": "`return 'pos'` is control-dependent on the `x > 0` arm" + }, + { + "symbol": "classify", + "filePath": "src/branch.ts", + "line": 11, + "note": "`return 'neg'` is control-dependent on the `x < 0` else-if arm" + }, + { + "symbol": "classify", + "filePath": "src/branch.ts", + "line": 13, + "note": "`return 'zero'` is control-dependent on the fallthrough arm" + } + ], + "inter_AIS": [], + "rationale": "Criterion is the if/else-if predicate chain in `classify` (line 7). CDG controller->dependent edges reach each return arm (lines 9, 11, 13); each runs only under its arm's condition. All dependents are intra-procedural; the function calls nothing. intra_AIS = the three arm returns; inter_AIS empty. Exercises CDG-forward over a multi-arm branch. Note the `else if (x < 0)` test on line 10 is itself nested control flow; the truly-affected executable statements are the three returns, which is what we annotate at statement granularity." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/src/branch.ts b/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/src/branch.ts new file mode 100644 index 000000000..964064724 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-branch/src/branch.ts @@ -0,0 +1,14 @@ +// Pure intra-procedural CONTROL-dependence fixture: an if/else-if/else. +// The branch predicate controls which return statement runs. Changing the +// branch (the criterion) control-affects all three arms via CDG, all within +// the same function. + +export function classify(x: number): string { + if (x > 0) { + // criterion: the branch predicate controls the arms below + return 'pos'; // control-dependent on the branch (first arm) + } else if (x < 0) { + return 'neg'; // control-dependent on the branch (second arm) + } + return 'zero'; // control-dependent on the branch (fallthrough arm) +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/ground-truth.json new file mode 100644 index 000000000..6d2ce880b --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/ground-truth.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "guarded", + "filePath": "src/guard.ts", + "direction": "downstream", + "marker": "y + 1", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "guarded", + "filePath": "src/guard.ts", + "line": 9, + "note": "`return -1` is control-dependent on the guard's true arm" + }, + { + "symbol": "guarded", + "filePath": "src/guard.ts", + "line": 11, + "note": "`const y = x * 2` runs only when the guard is false: control-dependent" + }, + { + "symbol": "guarded", + "filePath": "src/guard.ts", + "line": 12, + "note": "`const z = y + 1` is control-dependent on the guard (and data-dependent on y)" + }, + { + "symbol": "guarded", + "filePath": "src/guard.ts", + "line": 13, + "note": "`return z` is control-dependent on the guard" + } + ], + "inter_AIS": [], + "rationale": "Criterion is the guard predicate `if (!ok)` (line 7). Its CDG controller->dependent edges reach the true-arm `return -1` (line 9) and the post-guard body (lines 11-13), which execute only on the complementary arm. All dependents are statements WITHIN `guarded`; the function calls nothing. intra_AIS captures every control-dependent line; inter_AIS empty. This is the canonical guard-clause CDG shape (#559) and isolates the CDG-forward arm of KTD4 — PDG distinguishes control-affected statements that the call-graph mode cannot resolve below function granularity." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/src/guard.ts b/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/src/guard.ts new file mode 100644 index 000000000..b46214a23 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-guard/src/guard.ts @@ -0,0 +1,14 @@ +// Pure intra-procedural CONTROL-dependence fixture: the guard clause. +// The early-return guard predicate controls whether the post-guard body runs. +// Changing the guard (the criterion) control-affects the body statements via +// CDG controller->dependent edges, all within the same function. + +export function guarded(ok: boolean, x: number): number { + if (!ok) { + // criterion: the guard predicate controls the arms below + return -1; // control-dependent on the guard (true arm) + } + const y = x * 2; // control-dependent on the guard (false arm reaches here) + const z = y + 1; // control-dependent on the guard, data-dependent on `y` + return z; // control-dependent on the guard +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/ground-truth.json new file mode 100644 index 000000000..b5ffed554 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/ground-truth.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "filterPositive", + "filePath": "src/loop.ts", + "direction": "upstream", + "marker": "count + 1", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "filterPositive", + "filePath": "src/loop.ts", + "line": 10, + "note": "the inner `if (x > 0)` predicate directly controls the `count` increment" + }, + { + "symbol": "filterPositive", + "filePath": "src/loop.ts", + "line": 8, + "note": "the enclosing `for` loop guard controls whether the if and its body run" + } + ], + "inter_AIS": [], + "rationale": "Direction is UPSTREAM: the criterion `filterPositive`'s focal statement is the `count = count + 1` increment (line 11), and we ask which control predicates govern whether it runs. The immediate controller is the `if (x > 0)` (line 10); the if is itself nested in the `for` loop guard (line 8), which transitively controls the increment. Both controllers are intra-procedural. intra_AIS = {line 10 (inner if), line 8 (loop guard)}; inter_AIS empty. Isolates the CDG-reverse arm of KTD4 over nested control structure." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/src/loop.ts b/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/src/loop.ts new file mode 100644 index 000000000..506e0a607 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-control-loop/src/loop.ts @@ -0,0 +1,15 @@ +// Pure intra-procedural CONTROL-dependence fixture, annotated UPSTREAM. +// The criterion is the loop body; upstream asks "what controls whether this +// runs?" The answer is the enclosing loop guard. Exercises the CDG-reverse arm +// of KTD4 over a loop's control structure, all within one function. + +export function filterPositive(xs: number[]): number { + let count = 0; + for (const x of xs) { + // the loop guard controls the body below + if (x > 0) { + count = count + 1; // criterion (upstream): controlled by the loop AND the if + } + } + return count; +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/ground-truth.json new file mode 100644 index 000000000..a48554624 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/ground-truth.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "total", + "filePath": "src/accumulator.ts", + "direction": "downstream", + "marker": "sum + x", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "total", + "filePath": "src/accumulator.ts", + "line": 10, + "note": "for-loop body re-defines/uses `sum`: data-dependent on the initial def" + }, + { + "symbol": "total", + "filePath": "src/accumulator.ts", + "line": 12, + "note": "return reads the accumulated `sum`: data-dependent on every prior def" + } + ], + "inter_AIS": [], + "rationale": "Criterion is the function `total`, whose changed statement is the `sum` definition (line 8). `sum` is a loop-carried accumulator: its def reaches the in-loop redefinition/use (line 10) and the final return's use (line 12) via REACHING_DEF def->use edges. These two lines are the only truly-affected statements and they are all WITHIN `total` — so intra_AIS is exactly {line 10, line 12} and inter_AIS is empty (the function calls nothing, returns to no annotated caller in this single-function fixture). PDG should win precision here: the call-graph mode only knows `total` exists and has no inbound edges, so it reports `total` itself with no finer-grained intra-statement resolution." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/src/accumulator.ts b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/src/accumulator.ts new file mode 100644 index 000000000..6fa0705c2 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-accumulator/src/accumulator.ts @@ -0,0 +1,13 @@ +// Pure intra-procedural data-flow fixture. +// `sum` is a loop-carried accumulator: its definition flows forward to the +// next iteration's use and to the final `return`. Changing the `sum` def +// (the criterion) affects only statements within this same function via +// REACHING_DEF def->use edges. Nothing crosses a function boundary. + +export function total(xs: number[]): number { + let sum = 0; // criterion: the def of `sum` + for (const x of xs) { + sum = sum + x; // use of `sum` (and a redefinition) — data-dependent on the def + } + return sum; // use of `sum` — data-dependent +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/ground-truth.json new file mode 100644 index 000000000..94f0e31f1 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/ground-truth.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "chainCompute", + "filePath": "src/chain.ts", + "direction": "downstream", + "marker": "b - 3", + "pdgEdgeKinds": ["REACHING_DEF"] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { "symbol": "chainCompute", "filePath": "src/chain.ts", "line": 8, "note": "`b = a * 2` is directly data-dependent on the def of `a`" }, + { "symbol": "chainCompute", "filePath": "src/chain.ts", "line": 9, "note": "`c = b - 3` is transitively data-dependent on `a` via `b`" }, + { "symbol": "chainCompute", "filePath": "src/chain.ts", "line": 10, "note": "`return c` is transitively data-dependent on `a` via the chain" } + ], + "inter_AIS": [], + "rationale": "Criterion's changed statement is the def of `a` (line 7). A straight-line REACHING_DEF chain carries `a` -> `b` (line 8) -> `c` (line 9) -> `return` (line 10). Every downstream statement in the function is data-dependent on `a`; none escapes the function (no calls, no annotated caller). intra_AIS is exactly the three downstream lines; inter_AIS is empty. This case stresses transitive forward def->use reachability with no control structure, isolating the RD-forward arm of the KTD4 truth table." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/src/chain.ts b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/src/chain.ts new file mode 100644 index 000000000..053bc1d14 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-chain/src/chain.ts @@ -0,0 +1,11 @@ +// Pure intra-procedural data-flow fixture: a straight-line def->use chain. +// Changing the first definition `a` flows through `b` and `c` to the return, +// all within one function. There are no branches and no calls — the purest +// REACHING_DEF chain. + +export function chainCompute(input: number): number { + const a = input + 1; // criterion: the def of `a` + const b = a * 2; // data-dependent on `a` + const c = b - 3; // data-dependent on `b` (transitively on `a`) + return c; // data-dependent on `c` +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/ground-truth.json new file mode 100644 index 000000000..a7d107f26 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/ground-truth.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "reassignSum", + "filePath": "src/reassign.ts", + "direction": "upstream", + "marker": "total + b", + "pdgEdgeKinds": ["REACHING_DEF"] + }, + "locus": "intra", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { "symbol": "reassignSum", "filePath": "src/reassign.ts", "line": 7, "note": "first def `total = a` reaches the criterion use transitively via the second def" }, + { "symbol": "reassignSum", "filePath": "src/reassign.ts", "line": 8, "note": "second def `total = total + b` is the immediately-reaching def of the criterion use" } + ], + "inter_AIS": [], + "rationale": "Direction is UPSTREAM: the criterion `reassignSum`'s relevant statement is the `return total` use (line 9), and we ask which definitions reach it. The immediately-reaching def is line 8 (`total = total + b`); that def itself uses `total`, whose reaching def is line 7 (`total = a`). Both defs are upstream data-dependencies of the criterion use, both intra-procedural. intra_AIS = {line 7, line 8}; inter_AIS empty. This isolates the RD-reverse arm of the KTD4 truth table and a reassigned variable's def chain." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/src/reassign.ts b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/src/reassign.ts new file mode 100644 index 000000000..8f3a4314d --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/intra-dataflow-reassign/src/reassign.ts @@ -0,0 +1,10 @@ +// Pure intra-procedural data-flow fixture, annotated UPSTREAM. +// The criterion is the final `return total` (a use of `total`); upstream asks +// "what does this use depend on?" The answer is every reaching definition of +// `total` within the function. Exercises the reverse RD arm of KTD4. + +export function reassignSum(a: number, b: number): number { + let total = a; // first def of `total` — reaches the use below + total = total + b; // second def of `total` (uses prior) — reaches the use below + return total; // criterion: use of `total` +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/ground-truth.json new file mode 100644 index 000000000..b5344a209 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/ground-truth.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "computeAndEmit", + "filePath": "src/mixed.ts", + "direction": "downstream", + "marker": "score + v", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "mixed", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "computeAndEmit", + "filePath": "src/mixed.ts", + "line": 14, + "note": "loop body `score = score + v` re-defines/uses `score`: data-dependent" + }, + { + "symbol": "computeAndEmit", + "filePath": "src/mixed.ts", + "line": 16, + "note": "`level = score > 10 ? ...` is data-dependent on the accumulated `score`" + }, + { + "symbol": "computeAndEmit", + "filePath": "src/mixed.ts", + "line": 17, + "note": "the `emit(level)` arg is data-dependent on `level`/`score`" + } + ], + "inter_AIS": [ + { + "symbol": "emit", + "filePath": "src/mixed.ts", + "note": "called with the computed level; cross-function reach" + } + ], + "rationale": "Criterion `computeAndEmit` is mixed-locus. The `score` def (line 12) drives a loop-carried data chain (line 14), feeds the threshold `level` (line 16), which feeds the `emit` call arg (line 17): all intra-procedural data dependence, so intra_AIS holds those three statements. Separately, the call to `emit` is a cross-function reach captured in inter_AIS. The two sets are disjoint (lines within `computeAndEmit` vs the distinct symbol `emit`). Distinct from mixed-validate-then-call: this case's intra dependence is data-flow-dominated (accumulator + ternary) rather than guard-dominated, broadening mixed-stratum coverage." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/src/mixed.ts b/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/src/mixed.ts new file mode 100644 index 000000000..582372d49 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-compute-and-emit/src/mixed.ts @@ -0,0 +1,18 @@ +// Mixed-locus fixture: `computeAndEmit` has a real intra-procedural data-flow +// computation (a running `score` accumulated in a loop, then thresholded) AND a +// cross-function reach (it calls `emit`). The criterion's blast radius spans +// both loci. + +export function emit(level: string): string { + return '[' + level + ']'; +} + +export function computeAndEmit(values: number[]): string { + // criterion: mixed. Intra: score accumulation + threshold; inter: emit(). + let score = 0; // def; loop-carried accumulation below + for (const v of values) { + score = score + v; // data-dependent on prior `score` + } + const level = score > 10 ? 'high' : 'low'; // data-dependent on `score` + return emit(level); // cross-function reach; arg data-dependent on `level` +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/ground-truth.json new file mode 100644 index 000000000..46ee0b4b0 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/ground-truth.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "route", + "filePath": "src/mixed.ts", + "direction": "downstream", + "marker": "key === 0", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "mixed", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "route", + "filePath": "src/mixed.ts", + "line": 16, + "note": "the guard `if (urgent || key === 0)` is data-dependent on `key` and controls the arms" + }, + { + "symbol": "route", + "filePath": "src/mixed.ts", + "line": 18, + "note": "`return fast(n)` is control-dependent on the guard's true arm" + }, + { + "symbol": "route", + "filePath": "src/mixed.ts", + "line": 20, + "note": "`return slow(n)` is control-dependent on the complementary arm" + } + ], + "inter_AIS": [ + { + "symbol": "fast", + "filePath": "src/mixed.ts", + "note": "callee on the urgent/even route; cross-function reach" + }, + { + "symbol": "slow", + "filePath": "src/mixed.ts", + "note": "callee on the fallthrough route; cross-function reach" + } + ], + "rationale": "Criterion `route` is mixed-locus with BOTH a guard whose predicate reads the intra def `key` (line 15 -> line 16) and control-gates both return arms (lines 18, 20), AND two cross-function reaches (fast, slow). intra_AIS holds the guard + the two control-dependent return statements; inter_AIS holds the two callees. Disjoint by construction (lines within `route` vs distinct symbols). This mixed case combines control AND data dependence intra-procedurally with branching cross-function reach — the richest mixed shape, complementing the data-dominated and guard-dominated mixed cases." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/src/mixed.ts b/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/src/mixed.ts new file mode 100644 index 000000000..b052753e7 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-guarded-dispatch/src/mixed.ts @@ -0,0 +1,21 @@ +// Mixed-locus fixture: `route` has an intra-procedural guard chain that both +// computes a `key` (data flow) and control-gates which callee runs (control +// flow), AND reaches two callees cross-function. Both loci are non-trivial. + +export function fast(n: number): number { + return n; +} + +export function slow(n: number): number { + return n + n; +} + +export function route(n: number, urgent: boolean): number { + // criterion: mixed. Intra: key + guard; inter: fast()/slow(). + const key = n % 2; // def; used in the guard below + if (urgent || key === 0) { + // control + data dependent on `key` + return fast(n); // control-dependent on the guard; cross-function reach + } + return slow(n); // control-dependent on the complementary arm; cross-function reach +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/ground-truth.json new file mode 100644 index 000000000..9d6ba53b9 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/ground-truth.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "handleRequest", + "filePath": "src/mixed.ts", + "direction": "downstream", + "marker": "normalized + 1", + "pdgEdgeKinds": [ + "REACHING_DEF", + "CDG" + ] + }, + "locus": "mixed", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [ + { + "symbol": "handleRequest", + "filePath": "src/mixed.ts", + "line": 14, + "note": "the guard `if (normalized < 0)` is data-dependent on `normalized` and controls the arms" + }, + { + "symbol": "handleRequest", + "filePath": "src/mixed.ts", + "line": 16, + "note": "`return -1` is control-dependent on the guard" + }, + { + "symbol": "handleRequest", + "filePath": "src/mixed.ts", + "line": 18, + "note": "`const adjusted = normalized + 1` is data-dependent on `normalized`" + }, + { + "symbol": "handleRequest", + "filePath": "src/mixed.ts", + "line": 19, + "note": "the `persist(adjusted)` arg is data-dependent on `adjusted`/`normalized`; the return is control-dependent on the guard" + } + ], + "inter_AIS": [ + { + "symbol": "persist", + "filePath": "src/mixed.ts", + "note": "called with the adjusted value; cross-function reach" + } + ], + "rationale": "Criterion `handleRequest` is mixed-locus. The `normalized` def (line 13) drives BOTH a data chain (guard test line 14, `adjusted` line 18, the call arg line 19) and a control structure (the guard controls lines 16 and 19). So intra_AIS holds the four control/data-dependent statements within the function. Separately, `handleRequest` calls `persist` — a genuine cross-function reach — so inter_AIS holds `persist`. This is the case where the two modes are complementary: PDG resolves the intra statement set precisely, call-graph reaches the callee. intra_AIS and inter_AIS are disjoint by construction (one is lines within `handleRequest`, the other is a different symbol `persist`)." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/src/mixed.ts b/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/src/mixed.ts new file mode 100644 index 000000000..f30d7d01b --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/mixed-validate-then-call/src/mixed.ts @@ -0,0 +1,20 @@ +// Mixed-locus fixture: `handleRequest` has BOTH genuine intra-procedural +// dependence (a normalized value computed and reused across statements, guarded +// by a validity check) AND a cross-function reach (it calls `persist`). +// Changing it affects intra statements (the normalize->guard->use chain) AND +// the callee. + +export function persist(value: number): number { + return value; +} + +export function handleRequest(raw: number): number { + // criterion: mixed. Intra: normalized flows through the guard to the call. + const normalized = raw * 2; // def; data-dependent uses below + if (normalized < 0) { + // control-dependent guard + return -1; // control-dependent on the guard + } + const adjusted = normalized + 1; // data-dependent on `normalized` + return persist(adjusted); // cross-function reach; arg data-dependent on `adjusted` +} diff --git a/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/ground-truth.json b/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/ground-truth.json new file mode 100644 index 000000000..b72313898 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/ground-truth.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "criterion": { + "name": "Shape", + "filePath": "src/nobody.ts", + "direction": "downstream" + }, + "locus": "n/a", + "pdgScoring": "exclude", + "provenance": "manual", + "analyzerVersion": "1.6.7", + "intra_AIS": [], + "inter_AIS": [], + "rationale": "This is the KTD6 no-body case. `Shape` (an interface), `ShapeName` (a type alias), and `AbstractShape.perimeter` (an abstract method) have no CFG body, so they emit ZERO PDG (CDG/REACHING_DEF) edges. Their `intra_AIS` is genuinely undefined under PDG, not empty-because-safe — a confident `impactedCount:0/LOW` would be the exact false-safe the impact tool exists to prevent (#2129/#1858 lineage). The case is tagged `pdgScoring: \"exclude\"` and `locus: \"n/a\"` so the U7 harness drops it from PDG precision/recall denominators (and logs the exclusion, never scoring it 0/0). It exists to assert the exclusion path is honored — it is the only fixture the smoke test exempts from the 'criterion produces CDG+REACHING_DEF edges' requirement. intra_AIS and inter_AIS are both empty by definition (no body)." +} diff --git a/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/src/nobody.ts b/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/src/nobody.ts new file mode 100644 index 000000000..a5cae82b2 --- /dev/null +++ b/gitnexus/bench/impact-pdg/fixtures/nobody-interface-excluded/src/nobody.ts @@ -0,0 +1,16 @@ +// No-body fixture (the KTD6 case). An interface, a type alias, and an abstract +// method have NO CFG body, so they produce ZERO PDG edges. PDG mode cannot +// score these — a bare `impactedCount:0` would read as a confident "safe", +// the exact false-safe impact exists to prevent. The harness EXCLUDES this +// case from PDG scoring (pdgScoring: "exclude"); it exists to assert the +// exclusion path, not to be measured. + +export interface Shape { + area(): number; // criterion: an interface method declaration — no body, no CFG +} + +export type ShapeName = 'circle' | 'square'; + +export abstract class AbstractShape { + abstract perimeter(): number; // abstract method — no body +} diff --git a/gitnexus/test/integration/impact-pdg-fixtures.test.ts b/gitnexus/test/integration/impact-pdg-fixtures.test.ts new file mode 100644 index 000000000..128150002 --- /dev/null +++ b/gitnexus/test/integration/impact-pdg-fixtures.test.ts @@ -0,0 +1,356 @@ +import { describe, it, expect, afterAll } from 'vitest'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; + +// U6 — schema-validation + smoke test for the curated impact-PDG accuracy +// fixtures (bench/impact-pdg/fixtures). Two guards: +// +// 1. SCHEMA VALIDATION — every ground-truth.json is well-formed: required keys +// present, a valid `locus`, intra/inter AIS non-overlapping where +// applicable, and the corpus meets the KTD9/F3 minimum floor (>= 3 cases +// per locus stratum, >= 12 measurable). +// +// 2. SMOKE — each fixture analyzes cleanly under {pdg:true} and produces CDG + +// REACHING_DEF edges, AND the criterion function SPECIFICALLY produces both +// (located via the criterion `marker`). A criterion whose function emits +// ZERO PDG edges has unmeasurable ground truth — the smoke test catches it. +// The one intentional no-body case (pdgScoring:"exclude") is the sole +// exemption: its criterion must produce ZERO PDG edges (the KTD6 case). +// +// We annotate from SOURCE SEMANTICS only (KTD9 annotation-circularity guard) — +// this test does NOT derive ground truth from the traversal; it only confirms +// the fixtures are measurable substrate. Reconciling AIS against the live +// traversal is U7's job, not U6's. + +const FIXTURES_DIR = path.join(__dirname, '..', '..', 'bench', 'impact-pdg', 'fixtures'); + +const VALID_LOCI = new Set(['intra', 'inter', 'mixed', 'n/a']); +const VALID_DIRECTIONS = new Set(['downstream', 'upstream']); +const VALID_PROVENANCE = new Set(['manual', 'mutation']); + +interface AisEntry { + symbol: string; + filePath: string; + line?: number; + note?: string; +} +type PdgEdgeKind = 'REACHING_DEF' | 'CDG'; +interface Criterion { + name: string; + filePath: string; + direction: string; + marker?: string; + /** + * The PDG edge kinds the criterion function is EXPECTED to produce. A pure + * straight-line data-flow criterion legitimately produces only REACHING_DEF + * (no branches -> no control dependence); a branching/guard criterion + * produces both. The smoke test asserts exactly these are non-zero on the + * criterion, so a pure-dataflow archetype is not forced to carry an + * artificial branch. Omitted on excluded no-body cases. + */ + pdgEdgeKinds?: PdgEdgeKind[]; +} +interface GroundTruth { + schemaVersion: number; + criterion: Criterion; + locus: string; + pdgScoring?: string; + provenance: string; + analyzerVersion: string; + intra_AIS: AisEntry[]; + inter_AIS: AisEntry[]; + rationale: string; +} + +interface FixtureCase { + name: string; + dir: string; + gt: GroundTruth; + excluded: boolean; +} + +function loadFixtures(): FixtureCase[] { + const entries = fs + .readdirSync(FIXTURES_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); + return entries.map((name) => { + const dir = path.join(FIXTURES_DIR, name); + const gtPath = path.join(dir, 'ground-truth.json'); + const gt = JSON.parse(fs.readFileSync(gtPath, 'utf8')) as GroundTruth; + return { name, dir, gt, excluded: gt.pdgScoring === 'exclude' }; + }); +} + +const FIXTURES = loadFixtures(); + +function isAisEntry(e: unknown): e is AisEntry { + if (typeof e !== 'object' || e === null) return false; + const o = e as Record; + if (typeof o.symbol !== 'string' || o.symbol.length === 0) return false; + if (typeof o.filePath !== 'string' || o.filePath.length === 0) return false; + if (o.line !== undefined && (typeof o.line !== 'number' || !Number.isInteger(o.line))) { + return false; + } + return true; +} + +/** Stable key for an AIS entry so intra/inter overlap is set-comparable. */ +function aisKey(e: AisEntry): string { + return `${e.symbol}@${e.filePath}#${e.line ?? '-'}`; +} + +const tmpDirs: string[] = []; +function freshRepo(srcDir: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-impact-pdg-fx-')); + fs.cpSync(srcDir, dir, { recursive: true }); + tmpDirs.push(dir); + return dir; +} + +interface Counts { + basicBlocks: number; + reachingDefs: number; + cdg: number; +} +function counts(result: PipelineResult): Counts { + let basicBlocks = 0; + result.graph.forEachNode((n) => { + if (n.label === 'BasicBlock') basicBlocks++; + }); + let reachingDefs = 0; + let cdg = 0; + for (const rel of result.graph.iterRelationships()) { + if (rel.type === 'REACHING_DEF') reachingDefs++; + if (rel.type === 'CDG') cdg++; + } + return { basicBlocks, reachingDefs, cdg }; +} + +/** + * BasicBlock id template (cf. emit.ts): + * `BasicBlock::::` + * (filePath may itself contain `:` on some platforms). All blocks of one + * function share the anchor = id minus the trailing `:`. + */ +function functionAnchorOf(blockId: string): string { + return blockId.slice(0, blockId.lastIndexOf(':')); +} + +/** + * Locate the criterion function's block anchor via its `marker` (a substring + * unique to the function body, present in one of its blocks' `text`), then + * count CDG / REACHING_DEF edges SOURCED inside that function. Mirrors the + * `cdgSourcedInHazardFunction` technique in pipeline-pdg.test.ts: attributing + * edges to a specific function, not the whole-fixture aggregate. + */ +function criterionEdgeCounts( + result: PipelineResult, + marker: string, +): { cdg: number; reachingDefs: number; found: boolean } { + let anchor: string | undefined; + const idsByAnchor = new Map>(); + result.graph.forEachNode((n) => { + if (n.label !== 'BasicBlock') return; + const a = functionAnchorOf(n.id); + (idsByAnchor.get(a) ?? idsByAnchor.set(a, new Set()).get(a)!).add(n.id); + const text = (n.properties as { text?: string }).text ?? ''; + if (text.includes(marker)) anchor = a; + }); + if (anchor === undefined) return { cdg: 0, reachingDefs: 0, found: false }; + const blockIds = idsByAnchor.get(anchor) ?? new Set(); + let cdg = 0; + let reachingDefs = 0; + for (const rel of result.graph.iterRelationships()) { + if (!blockIds.has(rel.sourceId)) continue; + if (rel.type === 'CDG') cdg++; + if (rel.type === 'REACHING_DEF') reachingDefs++; + } + return { cdg, reachingDefs, found: true }; +} + +describe('U6 — impact-PDG fixture ground-truth schema', () => { + it('discovers the curated fixtures', () => { + expect(FIXTURES.length).toBeGreaterThan(0); + }); + + for (const fx of FIXTURES) { + describe(fx.name, () => { + const { gt } = fx; + + it('has all required top-level keys', () => { + expect(typeof gt.schemaVersion).toBe('number'); + expect(gt.criterion).toBeTypeOf('object'); + expect(VALID_LOCI.has(gt.locus)).toBe(true); + expect(VALID_PROVENANCE.has(gt.provenance)).toBe(true); + // v1 is manual-annotation-primary (KTD9 — mutation deferred). + expect(gt.provenance).toBe('manual'); + expect(typeof gt.analyzerVersion).toBe('string'); + expect(gt.analyzerVersion.length).toBeGreaterThan(0); + expect(Array.isArray(gt.intra_AIS)).toBe(true); + expect(Array.isArray(gt.inter_AIS)).toBe(true); + expect(typeof gt.rationale).toBe('string'); + // Rationale is what makes manual annotation defensible — non-trivial. + expect(gt.rationale.length).toBeGreaterThan(40); + }); + + it('has a well-formed criterion', () => { + const c = gt.criterion; + expect(typeof c.name).toBe('string'); + expect(c.name.length).toBeGreaterThan(0); + expect(typeof c.filePath).toBe('string'); + expect(c.filePath.startsWith('src/')).toBe(true); + expect(VALID_DIRECTIONS.has(c.direction)).toBe(true); + // The criterion source file actually exists. + expect(fs.existsSync(path.join(fx.dir, c.filePath))).toBe(true); + // Measurable cases carry a marker; excluded no-body cases need none. + if (!fx.excluded) { + expect(typeof c.marker, `${fx.name} needs a criterion.marker`).toBe('string'); + expect(c.marker!.length).toBeGreaterThan(0); + // The marker must appear in the criterion file's source. + const src = fs.readFileSync(path.join(fx.dir, c.filePath), 'utf8'); + expect(src.includes(c.marker!), `marker ${JSON.stringify(c.marker)} in source`).toBe(true); + // Measurable cases declare which PDG edge kinds the criterion produces. + expect(Array.isArray(c.pdgEdgeKinds), `${fx.name} needs criterion.pdgEdgeKinds`).toBe( + true, + ); + expect(c.pdgEdgeKinds!.length).toBeGreaterThan(0); + for (const k of c.pdgEdgeKinds!) { + expect(['REACHING_DEF', 'CDG'], `valid pdgEdgeKind ${k}`).toContain(k); + } + } + }); + + it('has well-formed, valid AIS entries', () => { + for (const e of gt.intra_AIS) { + expect(isAisEntry(e), `intra_AIS entry ${JSON.stringify(e)}`).toBe(true); + } + for (const e of gt.inter_AIS) { + expect(isAisEntry(e), `inter_AIS entry ${JSON.stringify(e)}`).toBe(true); + } + }); + + it('has non-overlapping intra/inter AIS', () => { + const intraKeys = new Set(gt.intra_AIS.map(aisKey)); + for (const e of gt.inter_AIS) { + expect(intraKeys.has(aisKey(e)), `inter entry ${aisKey(e)} overlaps intra`).toBe(false); + } + }); + + it('matches its locus to its AIS shape', () => { + if (gt.locus === 'inter') { + // Inter cases: PDG intra-AIS is empty by design; the impact is cross-function. + expect(gt.intra_AIS.length).toBe(0); + expect(gt.inter_AIS.length).toBeGreaterThan(0); + } else if (gt.locus === 'intra') { + // Intra cases: the truly-affected set is within the function. + expect(gt.intra_AIS.length).toBeGreaterThan(0); + expect(gt.inter_AIS.length).toBe(0); + } else if (gt.locus === 'mixed') { + // Mixed cases: both loci carry genuine impact. + expect(gt.intra_AIS.length).toBeGreaterThan(0); + expect(gt.inter_AIS.length).toBeGreaterThan(0); + } else { + // n/a: only the excluded no-body case; both AIS empty. + expect(fx.excluded).toBe(true); + expect(gt.intra_AIS.length).toBe(0); + expect(gt.inter_AIS.length).toBe(0); + } + }); + }); + } + + it('meets the KTD9/F3 minimum corpus floor (>=3 per locus stratum, >=12 measurable)', () => { + const byLocus = new Map(); + let measurable = 0; + for (const fx of FIXTURES) { + if (fx.excluded) continue; + measurable++; + byLocus.set(fx.gt.locus, (byLocus.get(fx.gt.locus) ?? 0) + 1); + } + expect(measurable).toBeGreaterThanOrEqual(12); + for (const locus of ['intra', 'inter', 'mixed']) { + expect(byLocus.get(locus) ?? 0, `>=3 cases for locus ${locus}`).toBeGreaterThanOrEqual(3); + } + }); + + it('exercises BOTH PDG edge kinds across the corpus', () => { + const declared = new Set(); + for (const fx of FIXTURES) { + if (fx.excluded) continue; + for (const k of fx.gt.criterion.pdgEdgeKinds ?? []) declared.add(k); + } + expect(declared.has('REACHING_DEF')).toBe(true); + expect(declared.has('CDG')).toBe(true); + }); + + it('has exactly one excluded no-body case (the KTD6 case)', () => { + const excluded = FIXTURES.filter((fx) => fx.excluded); + expect(excluded.length).toBe(1); + expect(excluded[0].gt.locus).toBe('n/a'); + }); +}); + +describe('U6 — impact-PDG fixtures analyze under {pdg:true} with measurable criteria', () => { + afterAll(() => { + for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); + }); + + for (const fx of FIXTURES) { + if (fx.excluded) { + // The intentional no-body case: its criterion must produce ZERO PDG edges + // (KTD6) — a confident zero is the whole point of tagging it for exclusion. + it(`${fx.name}: no-body criterion produces ZERO PDG edges (KTD6 exclusion)`, async () => { + const result = await runPipelineFromRepo(freshRepo(fx.dir), () => {}, { pdg: true }); + // The fixture has no function bodies at all, so the whole-repo PDG layer + // is empty too — but the load-bearing claim is the criterion symbol. + const { basicBlocks } = counts(result); + expect(basicBlocks, `${fx.name} no-body fixture should emit no BasicBlocks`).toBe(0); + }, 60000); + continue; + } + + it(`${fx.name}: analyzes under --pdg; criterion produces its declared PDG edges`, async () => { + const result = await runPipelineFromRepo(freshRepo(fx.dir), () => {}, { pdg: true }); + + // The fixture as a whole produces a PDG layer (BasicBlocks + RD edges). + // CDG is not asserted fixture-wide: a pure straight-line data-flow + // fixture legitimately has zero control dependence. + const total = counts(result); + expect(total.basicBlocks, `${fx.name} BasicBlock count`).toBeGreaterThan(0); + expect(total.reachingDefs, `${fx.name} fixture REACHING_DEF count`).toBeGreaterThan(0); + + // The CRITERION function specifically — located by its marker — must + // produce EXACTLY the edge kinds its ground truth declares, and at least + // one PDG edge overall. A zero-edge criterion has unmeasurable ground + // truth; this is the load-bearing smoke gate (no accidental no-body). + const marker = fx.gt.criterion.marker!; + const crit = criterionEdgeCounts(result, marker); + expect( + crit.found, + `${fx.name} criterion blocks located via marker ${JSON.stringify(marker)}`, + ).toBe(true); + expect( + crit.cdg + crit.reachingDefs, + `${fx.name} criterion '${fx.gt.criterion.name}' produces >=1 PDG edge (unmeasurable if 0)`, + ).toBeGreaterThan(0); + const kinds = fx.gt.criterion.pdgEdgeKinds ?? []; + if (kinds.includes('REACHING_DEF')) { + expect( + crit.reachingDefs, + `${fx.name} criterion declares REACHING_DEF but produced none`, + ).toBeGreaterThan(0); + } + if (kinds.includes('CDG')) { + expect( + crit.cdg, + `${fx.name} criterion declares CDG but produced none`, + ).toBeGreaterThan(0); + } + }, 60000); + } +});