A real-DB downstream line seed reaches a dependent block via the CDG edge, so
calleesOfBlocks runs over real seed+reachable blocks whose `callees` carry no
data (created without the property — the no-calls / pre-v2 reality). Assert the
call completes without surfacing a query failure, the slice resolves, and the
statement-precise inter-procedural precision is null (empty reach) rather than a
partial value. The column-absent variant is unit-covered (forced-throw test);
the harness builds the full schema so it cannot create a column-less table.
Addresses PR #2227 tri-review finding (testing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the slice-callees query (RETURN b.callees) fails, calleesOfBlocks logs and
returns an empty set, so the bridge is not built and inter-procedural reach falls
back to callgraph-equal — no error surfaces and no partial proven/unproven label
is produced. Add a dispatch test that throws on the callees query (via
vi.mocked, strictly typed) and asserts no bridge is passed to the BFS and no
error surfaces.
Addresses PR #2227 tri-review finding (P1-testing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
statementPreciseByDepth / *Counts / *ImpactedCount / statementPrecision were
emitted both at the top level of the impact result and nested under
pdgInterprocedural, doubling the contract surface and risking incoherence if one
were mutated. Keep them only under pdgInterprocedural (the scoped namespace),
drop the top-level duplicates from PdgImpactBaseResult and the composer, and
point the blast-radius reader at the single nested path (no fallback chain).
Addresses PR #2227 tri-review finding (maintainability).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dispatch read reachableBlocks/seedBlocks and the composer read pdgEvidence
via `(pdgResult as any)`. Replace both with a discriminated-union narrow: the
slice fields live only on the success/empty results, so narrowing with the same
`'error' in / 'pdgLayer' in` guard the composer already uses yields the typed
string[] / PdgImpactEvidenceSummary without a cast. No `as any` remain on
pdgResult; behaviour is unchanged.
Addresses PR #2227 tri-review finding (maintainability) and the strict-typing
requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The statement seed query matches blocks by startLine within the symbol's span
(a forgiving window), so a closure body block that starts on the SAME source
line as the seeded statement — but is owned by a different (nested) function —
leaked into the seed and contaminated the intra slice with the closure's
dependence. Filter the seed blocks to those whose owning fnLine === sym.startLine
+ 1 (block ids encode the 1-based function start line). Defensive: the filter
only applies when it leaves >=1 seed, so a symbol kind whose fnLine convention
differs never loses a real seed.
Addresses PR #2227 tri-review finding (P3, adversarial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A symbol reachable from multiple parents in the inter-procedural BFS got its
proven/unproven bridge label from whichever parent the DB returned first
(first-writer-wins), so a diamond-reachable depth≥2 symbol could flip label
run-to-run. Compute evidence for every edge, keep the strongest across all
parents (callgraph-bridge wins, via betterBridgeEvidence), and stamp the
finalized label onto the impacted items after the depth loop. The label is now
deterministic; reach is unaffected.
Addresses PR #2227 tri-review finding (P3, correctness + adversarial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The statement-precise projection dropped a callee invoked directly on the
changed line when that callee was not also called from a downstream-dependent
block: sliceCalleeNames was built only from reachableBlocks, which excludes the
seed block by the seed-minus-reachable convention. Such a callee — the most
directly impacted of all — was labeled unproven-bridge and dropped from
statementPreciseByDepth, deflating statementPrecision.
runImpactPDG now surfaces its seedBlocks (threaded through assemblePdgImpactResult
and both empty/no-reachability early returns), and the dispatch unions
calleesOfBlocks(seedBlocks ∪ reachableBlocks). A callee on the changed line is
now proven. Recall was already preserved (full interproceduralByDepth unchanged);
this fixes the precision label.
Verified: parseSourceSafe@231 statementPrecision 0.500 → 0.667 (the seed-line
callee is now proven). New dispatch regression test covers the empty-reachable /
non-empty-seed case.
Addresses PR #2227 tri-review headline (P2, Codex + correctness + adversarial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bridge rewrite replaced the old `rel[7]` call-site-line parse with
`rel.name`, leaving `r.reason AS relationReason` selected in both _runImpactBFS
queries but never read — dead payload fetched on every BFS depth step. Remove it.
Addresses PR #2227 tri-review finding (P3, Codex + maintainability).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new `BasicBlock.callees` column changes the BasicBlock node CSV header (and
thus the byte-identity fingerprint over all emitted CSV lines), tripping the
emit-persistence --check gate that the CI benchmarks job runs. This is the gate
working as designed — a legitimate, reviewed emit change.
Regenerate both committed baselines (whole-graph + streaming PdgEmitSink) per the
documented procedure. The streaming gate still confirms byte_identical_nodes/edges
and resident_basic_blocks === 0, so the schema change preserves the streaming
invariants.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make PDG impact mode's cross-function reach a real precision win instead of a
flat tie with callgraph. The proven/unproven-bridge labeling was degenerate:
it tried to read a call-site line off the CALLS edge, but edges carry only
{type, confidence, reason} (no line), so downstream was always "unproven" and
upstream always defaulted "proven" — neither a real signal.
Fix: persist what the CFG already harvests but dropped. `BasicBlock.callees`
(new STRING column) now carries the space-joined leaf callee names invoked in
each block, extracted in `emitFileCfgs` from the per-statement `sites`. The
impact bridge then marks a first-hop callee "proven" (callgraph-bridge) iff its
name appears in the callees of a block in the changed line's dependence slice,
else "unproven-bridge" — a sound, statement-precise discriminator.
`mode:'pdg'` gains an additive `statementPreciseByDepth` (+ counts,
`statementPreciseImpactedCount`, `statementPrecision`): the proven subset of the
inter-procedural reach. The full `interproceduralByDepth` is unchanged and still
preserves callgraph reach, so nothing is lost — the precise view sits alongside.
Measured on the live GitNexus index (240 functions): full reach stays identical
to callgraph (still no false "finds more"), and the statement-precise subset is
strictly tighter than callgraph on 43/90 with-slice functions (median proven 1
vs callgraph 2 symbols). Ground-truth `measure.mjs --check` stays green (native
PDG/callgraph F1 unchanged). Latency is ~1.2-1.6x (the extra slice-callees
lookup) — precision, not speed, as scoped.
INCREMENTAL_SCHEMA_VERSION → 2 (the new column forces a full re-analyze of
pre-v2 indexes; absent column degrades gracefully to the prior behavior).
`blast-radius.mjs` + its unit test gain the statement-precise axis; README's
four-axis verdict updated. Removes the dead `parseRelationSiteLine`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bench/impact-pdg/blast-radius.mjs: a real-code sampler that quantifies
HOW MUCH PDG narrows the impact set versus the pre-PDG (callgraph-only)
answer. Per real function it compares the line-seeded PDG statement slice to
the whole function body (block units), checks the PDG inter-procedural symbol
set against callgraph, and times both engines.
Measured on the live GitNexus index (240 functions, both directions): the PDG
slice is a median 0.30 (downstream) / 0.22 (upstream) of the function body and
localizes below whole-body on 240/240 functions, while its inter-procedural
symbol set is identical to callgraph on 240/240 (0 divergence) and it runs
~1.2-1.6x slower. Combined with the AIS-backed measure.mjs gate (intra/mixed
PDG F1=1.0, FPIS=FNIS=0), this is the proof that PDG's narrower slice is a
correct over-approximation cut, not a dropped-truth risk.
README gains a four-axis verdict that tests each "better" claim and reports it
honestly: tighter/fewer-false-alarms CONFIRMED, catches-callgraph-misses
CONFIRMED (new statement axis), finds-more-callers REFUTED (tie by design),
faster REFUTED. Adds a deterministic helper unit test (no analyze/DB).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bench/impact-pdg/real-code.mjs: a latency + quality-proxy probe that
runs both impact engines against an already-indexed real repo (default
GitNexus) and checks that unified mode:'pdg' preserves the callgraph
inter-procedural symbol set, what it costs, and how honest its PDG
evidence/degraded signals are. Unlike measure.mjs (the AIS-backed fixture
accuracy gate), a real repo has no curated ground truth, so this reports
quality proxies, not accuracy.
Each default case is anchored on a CFG block-start line so every case
exercises a real intra-procedural slice; a mid-block anchor degrades to
pdg-no-block-at-line, which the harness still detects and counts.
Measured on the live GitNexus index (PDG layer via analyze --pdg, ~171k
BasicBlocks): unified pdg reproduces callgraph symbol reach exactly
(recall = precision = 1.0 on all cases), ~1.2-1.4x latency overhead, and
downstream statement-anchored seeds correctly label out-of-slice reach
unproven-bridge (an expected proven-vs-reachable signal, not a regression).
Adds a deterministic helper unit test (pure metric math, no analyze/DB) and
README docs covering the probe, its gates, and a representative run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the harness to seed PDG on criterion.line and score at LINE
granularity vs intra_AIS (callgraph stays symbol vs inter_AIS). The
measurement is now real and non-empty:
intra pdg line/intra P=R=F1=1.000 (6 fixtures, FPIS=FNIS=0)
mixed pdg line/intra P=R=F1=1.000 (3)
inter cg symbol/inter P=R=F1=1.000 (3)
mixed cg symbol/inter P=R=F1=1.000 (3)
Verdict: PDG mode is exact at intra-procedural STATEMENT granularity
(which statements depend on a change); callgraph is exact at inter-
procedural SYMBOL granularity (what calls/uses a symbol). Different
questions, neither dominates, they compose. The old 'PDG empty /
callgraph wins' verdict was a whole-symbol-seed artifact.
Adds criterion.line to each fixture (source-semantics first). 6 fixtures'
intra_AIS reconciled to block granularity (CFG coalesces consecutive
stmts into one block; combined CDG+RD slice reaches more) — documented
per-rationale + as the #1 validity threat (annotation circularity).
inter/pdg P=0 is honest: a dispatcher's intra routing-returns aren't the
cross-function impact. --check exit 0; 105 tests green.
The whole-symbol pdg seed was structurally empty: seeding a function's
entire blocks and excluding seeds leaves nothing (intra-procedural reach
stays inside the function). Add a 'line' statement anchor: impact({mode:
'pdg', line:N}) seeds the dependence BFS on the BasicBlock at 1-based
source line N within the symbol and returns the dependent STATEMENTS
(affectedStatements: {line,filePath,text}[], affectedStatementCount,
criterionLine). Verified: impact --mode pdg total --line 8 on the
accumulator returns lines [10,12] = the ground-truth slice.
- blockAnchorForStatement (a.startLine = line, no +1 — block lines are
1-based and match source; bounded to the symbol span).
- pdgStatementsForBlocks resolves reachable blocks to source statements.
- line validated: PDG-only, positive integer; rejected on callgraph.
- Whole-symbol pdg keeps an honest empty note steering to line:<N>.
- tools.ts schema + CLI --line + eval-server statement render.
Refs: redesign after the harness/maintainer caught that symbol-level
pdg impact is empty in v1.
* test(lbug): lock PARALLEL=false as a tested correctness invariant (#2203)
The parallel CSV reader (Kuzu-derived, default PARALLEL=true) cannot parse
quoted fields with embedded newlines (kuzudb/kuzu#5778); our content/text
columns hold source code, so PARALLEL=false is mandatory for correctness.
Add a live-DB multiline-quoted round-trip that fails if it is ever flipped,
plus a static guard on the generated COPY queries. Export COPY_CSV_OPTS /
getCopyQuery for the static assertion; document the invariant at the source.
* feat(lbug): expose node/rel phase boundary via onNodePhaseComplete hook (#2203)
streamAllCSVsToDisk now fires an optional onNodePhaseComplete(nodeFiles)
callback right after node CSVs are flushed and before the relationship pass
writes any rel_*.csv — the boundary the COPY-overlap leg needs. The node-file
manifest construction is hoisted above the rel pass and reused in the return,
so output is byte-for-byte identical when no callback is supplied (verified by
the emit bench fingerprint and the splitRelCsvByLabelPair differential oracle).
The callback is not awaited, so the rel pass runs concurrently with the
caller's node COPY.
* perf(lbug): overlap node COPY with relationship emit (#2203)
The deferred parallelism leg of #2203. LadybugDB is single-writer and its
parallel CSV reader is unsafe for our multiline content (kuzudb/kuzu#5778), so
the only safe parallelism is pipeline-overlap: node COPY (uses conn, never the
rel files) runs concurrently with the relationship emit pass (writes rel_*.csv,
never conn). Node COPY now starts at streamAllCSVsToDisk's onNodePhaseComplete
boundary while the rel pass keeps writing; the relationship COPY still waits for
node COPY (FK precondition), so DB load order and content are unchanged.
- Extract copyNodeCSVs; start it in the hook (overlap) or after emit (serial).
- GITNEXUS_SERIAL_LBUG_LOAD=1 forces the legacy strictly-sequential path
(operator escape hatch + differential-test oracle).
- Settle the in-flight node-COPY promise on emit failure (no unhandled
rejection); rethrow node-COPY errors at the FK barrier.
- Preserve the PDG manifest merge + collision guards (node merge at the hook,
rel merge before rel COPY) and all retry/fallback/cleanup behavior.
- PROF_LBUG_LOAD gains mode=overlap|serial; copy-nodes becomes the residual
node-COPY time after emit (trends to 0 as overlap hides it).
* test(lbug): differential gate — overlap load === serial load (#2203)
Loads one fixture (multiple node tables, multiple edge pairs, multiline File
content + BasicBlock text) into two fresh DBs — once via the default node-COPY
‖ rel-emit overlap, once via GITNEXUS_SERIAL_LBUG_LOAD=1 — and asserts the two
databases are content-equivalent: identical per-table node counts, per-type
edge counts, byte-for-byte multiline content/text, and identical
insertedRels/skippedRels/warnings. This is the issue's byte-identical-content
acceptance gate for the parallelism leg.
* fix(review): apply autofix feedback
- csv-generator: onNodePhaseComplete doc-contract now matches reality (a sync
throw is allowed and is how loadGraphToLbug surfaces the manifest collision
guard) — drops the inaccurate 'must not throw synchronously' line.
- lbug-adapter: copyNodeCSVs totalSteps is the node-table count (drop the +1
rel-step holdover; the rel COPY has its own progress line).
- lbug-adapter: on emit+node-COPY double-failure, log the swallowed node-COPY
error before rethrowing the emit error (diagnosability).
- lbug-load-prof test: assert mode=overlap on the default path.
* fix(test): use mkdtemp for secure temp dirs (CodeQL js/insecure-temporary-file)
CodeQL flagged lbug-load-overlap.test.ts writing a file into a predictable
os.tmpdir() path. Create the base temp dir with fs.mkdtemp (atomic, random
suffix) in both new live-DB tests, and switch to the gitnexus-lbug- prefix that
TEST_FIXTURE_PREFIXES recognizes so the Windows stale-sidecar sweep covers
these fixtures.
* fix(lbug): check PDG manifest rel-pair collision before node COPY (#2203)
Found by Codex in tri-review. The manifest rel-pair collision guard ran after
the FK barrier (after node COPY committed), so on that should-never-happen
error branch the overlap path left orphan node rows AND the
GITNEXUS_SERIAL_LBUG_LOAD escape hatch diverged from the legacy 'validate
manifest before any COPY' behavior. Move the rel merge + collision check ahead
of beginNodeCopy/the barrier: the serial path now detects a collision before
committing any node rows (legacy parity restored — the escape hatch is a
faithful oracle again), and the overlap path detects it as early as csvResult
is available. The node-collision guard already ran before node COPY (in the
hook).
* test(lbug): cover rel-emit failure with node COPY in flight (#2203)
Resolves a P1 review gap on PR #2226: the overlap's catch(emitErr) branch
(settle the in-flight node-COPY promise, then rethrow the emit error) was
untested. Fault-injects via a vi.mock of streamAllCSVsToDisk that fires
onNodePhaseComplete (starting a real node COPY on a live DB) then throws,
asserting loadGraphToLbug rejects with the emit error and no unhandled
rejection leaks. Also covers the both-fail case (node COPY error is logged,
emit error still wins). Listener removed in finally; macrotask queue flushed
before the assertion so it can't pass vacuously.
* test(lbug): cover node-COPY hard-failure rethrow at the FK barrier (#2203)
Resolves the second P1 review gap on PR #2226. Mocks emit to fire
onNodePhaseComplete with a nodeFiles entry pointing at a missing CSV (a
bind-time COPY error that IGNORE_ERRORS does not suppress) and otherwise
succeed, so copyNodeCSVs throws, the error is captured in nodeCopyError, and
loadGraphToLbug rethrows it at the FK barrier — asserted via rejects /COPY
failed for File/.
* test(lbug): cover PDG manifest rel-pair collision in overlap + serial (#2203)
Resolves the P2 gap behind the Codex tri-review finding: the manifest rel-pair
collision guard (moved ahead of node COPY in ad195582) had no test. A leaky
graph with a structural BasicBlock->BasicBlock edge (routed by id-prefix, no
BasicBlock nodes — isolating the rel-pair clash from the node-CSV one) plus a
PdgEmitSink manifest declaring the same pair makes loadGraphToLbug reject with
the rel-pair collision error, asserted on both the overlap (default) and serial
(GITNEXUS_SERIAL_LBUG_LOAD=1) paths.
* perf(lbug): yield the event loop periodically during relationship emit (#2203)
Resolves a P2 review finding on PR #2226: the relationship-emit loop ran long
synchronous stretches between write-stream drain awaits, which could starve the
overlapped node-COPY callbacks on fast I/O and erode the node-COPY-||-rel-emit
overlap. Yield via setImmediate every REL_YIELD_EVERY (5000) edges so the node
COPY and drains get scheduling time. Scheduling-only — emit bench fingerprint
unchanged (byte-identical), csv-pipeline determinism + overlap differential
green.
* refactor(lbug): extract shared copyCsvWithRetry helper (#2203)
Resolves a P2 maintainability finding on PR #2226: the COPY + IGNORE_ERRORS
retry block was duplicated in copyNodeCSVs and the inline relationship-COPY
loop. Extract copyCsvWithRetry(conn, query, onError); the callback receives the
RAW retry error so each site keeps its own message shape + slice length (node
throws, slices 200; relationship warns + records the failed pair, slices 80).
Behavior-preserving — guarded by the live-DB round-trips plus the new
node-COPY-failure and overlap error-path tests.
* docs(lbug): document loadGraphToLbug non-transactionality (#2203)
Resolves the advisory review finding on PR #2226: loadGraphToLbug runs
independent COPYs with no surrounding transaction, so a mid-load failure leaves
a partial DB and recovery is a --force re-analyze. Make that contract explicit
on the function so callers don't assume atomicity.
Code-review fixes for the PDG impact mode:
- P1: _runImpactPDG re-resolved its seed by bare name, dropping the
file_path/target_uid/kind disambiguation the dispatch already applied
(wrong-symbol blast radius for ambiguous names). Anchor the seed from
the resolved sym via new blockAnchorForResolvedSymbol (no re-resolve);
resolveBlockAnchor untouched. +test (same-name funcs, file_path/uid).
- Degraded/no-body/no-dependence returns now share emptyPdgParityFields
(KTD8 shape parity for programmatic consumers).
- Drop dead offset/summaryOnly params; delete stale stub JSDoc.
- pdgLayerStatus unknown-probe wrapped in try/catch + uses its result.
- projectBlocksToSymbols no longer swallows DB errors as no-match.
- Seed-query truncation now sets truncated/truncatedBy:'limit'.
- Harness: try/finally cleans the work temp dir on callTool throw.
- Tests: pin REACHING_DEF exclusion, CDG controller-P precision guard,
Windows drive-colon path projection.
Root prettier --write across the feature's changed files (whitespace
only; ground-truth.json values and the data-based annotation fingerprint
unchanged — --check still PASS). Remove an unused loadMeta import flagged
by eslint (unused-imports/no-unused-imports).
measure.mjs drives both impact modes over the U6 corpus via a mock-free
real-analyze substrate (temp GITNEXUS_HOME + child-process analyze
--pdg + LocalBackend), validates fixtures in Step 0 (>=1 PDG edge + R4
same-line guard), and computes per-mode/per-scope precision/recall/F1 +
Jaccard + true/noise set-diffs (Arnold-Bohner CIS/AIS). Two-gate --check
(one-sided F1 band + annotation fingerprint, median-of-K for substrate
noise). Pure scorer in metrics.mjs; 18 synthetic-set metric-math unit
tests stay out of the flaky pipeline lane.
Key measured finding (the deliverable verdict): at SYMBOL granularity
PDG mode's intra blast radius is empty (intra-procedural dependence
collapses onto the criterion's own symbol), so callgraph wins for
symbol-level impact; PDG v1's value is block-level dependence detail.
Promotion gated on a deferred Function->BasicBlock CONTAINS_BLOCK edge.
Refs U7
formatImpactResult gains a PDG-aware branch (detected on mode:'pdg')
that returns before any callgraph path: renders findings as
'PDG-dependent symbols' (not 'depth N'), prints the intra-procedural
caveat, surfaces ambiguous/unresolved/truncation honestly, routes the
degradation note to 'analyze --pdg' guidance and the no-body note to a
KTD6 caveat (never 'isolated'). callgraph rendering byte-identical.
ai-context adds a hasPdg-gated --mode pdg hint.
Refs U5
projectBlocksToSymbols maps reachable BasicBlocks to owning Function/
Method nodes (startLine = fnLine-1, params bound). Same-line collisions
have no joinable column (no startColumn in schema), so report ALL
colliding symbols (ambiguous-projection), never silent-pick; blocks
with no owner surface under an 'unresolved' marker, never dropped.
Assemble a KTD8 parity result: byDepth collapsed to one bucket,
byDepthCounts, target identical, empty processes/modules, PDG-specific
epistemic, 'UNKNOWN' risk sentinel. mergeRisk exported (no logic change)
to guard that UNKNOWN never coerces to a confident LOW.
Refs U4
_runImpactPDG resolves the target to BasicBlocks (resolveBlockAnchor,
toolName union widened to 'impact') and runs a direction-aware bounded
BFS over CDG+REACHING_DEF: downstream = forward on both edge types,
upstream = reverse on both, unified in one anchored r.type IN [...]
query so the sense never diverges. Anchored + param-bound + LIMIT-
validated (KTD11). No-body symbols return a distinct KTD6 note (never a
confident zero); dual depth/limit truncation flags. fnLineOf extracted
to module scope; pdg_query byte-identical. Test-first per the plan.
Refs U3
Extract the 3-state meta-probe from _pdgQueryImpl into pdgLayerStatus,
a both-caps helper returning no-layer / sub-layer-missing / ready /
unknown without scanning the DB (except one bounded LIMIT 1 probe when
meta is unreadable). _pdgQueryImpl now consumes a single-cap projection
(pdgStampForMode) byte-identically. Wired into _impactImpl's pdg branch
so degraded indexes return a distinct note before the stub.
Refs U2
Add mode:'callgraph'|'pdg' to the impact MCP tool. callgraph (default)
is byte-identical; pdg routes to a stub (U3/U4). Backend hard-validates
the enum (schema enum is advisory), rejects relationTypes/crossDepth/
minConfidence and @group targets under pdg, and forks the ambiguous
branch so no callgraph BFS runs under pdg. CLI --mode wired.
Refs U1 of docs/plans/2026-06-16-001-feat-pdg-impact-mode-and-accuracy-harness-plan.md