test(impact-pdg): curated ground-truth fixture corpus (U6)

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
This commit is contained in:
Gergo Magyar 2026-06-16 08:41:19 +00:00
parent f63f3ebeac
commit 6e6c300d48
28 changed files with 1144 additions and 0 deletions

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

@ -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`)."
}

View file

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

View file

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

View file

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

View file

@ -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<string, unknown>;
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>:<fnStartLine>:<fnStartCol>:<blockIndex>`
* (filePath may itself contain `:` on some platforms). All blocks of one
* function share the anchor = id minus the trailing `:<blockIndex>`.
*/
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<string, Set<string>>();
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<string>();
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<string, number>();
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<string>();
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);
}
});