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
* perf(lbug): add PROF_LBUG_LOAD persistence-path timing breakdown (#2203 U1)
loadGraphToLbug is un-timed today; the analyze 'emit' number is the
scope-resolution emit bucket, not the CSV->COPY persistence path. Add a
zero-cost-when-off per-stage breakdown (csv-emit/copy-nodes/rel-split/
copy-rels/fallback/total + node/rel counts) gated by PROF_LBUG_LOAD=1,
mirroring the PROF_SCOPE_RESOLUTION pattern. Document the flag in README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(lbug): route relationships to per-pair CSVs in the emit pass (#2203 U2)
Relationships were written once to a monolithic relations.csv, then re-read
line-by-line (regex per edge) and re-split into per-FROM->TO-label-pair files
before COPY — writing and reading the entire ~1M-edge set twice. Route each
edge to its pair file directly during the single emit pass via a shared
RelPairRouter, eliminating the monolithic write + re-read + per-edge regex.
The router applies the SAME getNodeLabel + validTables filter as the legacy
splitRelCsvByLabelPair, which is retained as a differential oracle. A new
differential test asserts the direct-emit per-pair files are byte-for-byte
identical to the oracle's, with identical skip/total accounting. The prof
line (U1) drops its rel-split stage (routing now folds into csv-emit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(lbug): skip per-row microtask tick in BufferedCSVWriter (#2203 U3)
addRow awaited an already-resolved promise on every buffered row, scheduling
a microtask per node even when nothing flushed (millions at scale). It now
returns a promise ONLY when it flushes; the node-emit loop awaits once per
iteration after the switch. Flush/drain semantics are unchanged, so
backpressure on the rows that actually write is preserved and the emitted
CSV bytes are byte-identical (covered by the determinism + differential tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* bench(lbug): emit throughput + byte-identity gate for the persistence path (#2203 U4)
Build-free bench (bench/emit-persistence/measure.mjs) times streamAllCSVsToDisk
on a synthetic graph at two scales and gates: (1) an order-independent sha256
fingerprint over every emitted CSV line — the byte-identity guard for the U2/U3
emit optimisations — and (2) a scaling-ratio budget catching an O(n^2) emit
re-regression. Wired into ci-tests.yml alongside the cfg/scope-capture benches.
The LadybugDB COPY half needs a real DB, so its timing stays in PROF_LBUG_LOAD
+ the integration round-trip tests (documented in the bench README, with the
deferred COPY-parallelism follow-up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(review): apply autofix feedback (#2203)
- P1: router backpressure drain-await rejected with a generic AbortError,
masking the real EMFILE/disk-full error. Expose RelPairRouter.lastError and
rethrow it in the emit catch — mirrors the oracle's throw streamError ?? err.
- P1: cover RelPairRouter error + backpressure + teardown paths with a new
unit test (test/unit/rel-pair-routing.test.ts) using an injected mock stream.
- P2: wrap streamAllCSVsToDisk body in try/finally so the setMaxListeners bump
is always restored (the U2 rel-routing throw path could leak it).
- P2: dedup WriteStreamFactory — re-export the canonical type from
rel-pair-routing instead of a second identical declaration.
- P2: annotate splitRelCsvByLabelPair @internal as the retained differential
oracle so a future dead-code sweep doesn't delete the byte-identity guard.
- P3: differential test now covers the proc_ prefix + clears
GITNEXUS_SORT_GRAPH_OUTPUT to prevent env-leak desync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(lbug): scope byte-identity to quote-free ids + lock the quote-in-id divergence (#2215 review)
The 'byte-identical' claim was unconditional, but the router derives labels from
the raw id while the retained splitRelCsvByLabelPair oracle re-derives them via a
regex over the escaped row — so for an id containing a double-quote they diverge
(the router is the more-correct path). Soften the wording in rel-pair-routing.ts,
the bench README, and the differential-test comment to document the exception,
and add a differential test asserting the intended divergence (router routes the
quote-in-id edge; oracle drops it) so a future change can't silently revert to
the buggy regex semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* bench(lbug): per-file fingerprint so the gate catches pair-file mis-routing (#2215 review)
fingerprintEmit flattened every line of every per-pair file into one array,
sorted globally, and hashed — losing file boundaries, so a row routed to the
WRONG pair file produced an identical fingerprint. Hash a per-file digest
(filename + sha256(file bytes)) and combine the sorted entry list, so mis-routing
(and within-file row reordering) now changes the fingerprint. Baseline
regenerated; the new scheme yields a different hash on byte-identical emit,
confirming it is sensitive to file structure the old flatten ignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* bench(lbug): add absolute large-scale wall-time backstop to the emit gate (#2215 review)
The scaling-ratio gate only compares large/small, so a uniform Nx slowdown at
both scales passes with ratio ~1.0. Add an opt-in max_ms_large ceiling (1000ms
vs observed ~200ms — generous, host-noise-tolerant) that --check enforces
alongside the ratio, catching a gross absolute regression the ratio misses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(lbug): cover the sorted-output path in the byte-identity differential (#2215 review)
The differential test only exercised the default insertion-order emit path. Add
a case under GITNEXUS_SORT_GRAPH_OUTPUT=1 that feeds the oracle the same
id-sorted order orderedRelationships() uses and asserts per-pair byte-identity,
so within-pair row reordering on the sorted path can't slip past the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(lbug): cover the invalid-TO-label skip branch (#2215 review)
Only an invalid-FROM label was exercised; the validTables skip is an OR over
both endpoints, so the invalid-TO branch was untested (an inverted && would
have slipped through). Add a valid-FROM/invalid-TO edge to the differential
test and the router unit test, asserting it's skipped identically by router and
oracle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(lbug): exercise the BufferedCSVWriter FLUSH_EVERY boundary in vitest (#2215 review)
The U3 addRow change (returns a flush promise only on flush; undefined when
buffered) and the loop's `if (pending) await pending` were only crossed by the
bench, never vitest (all fixtures are <500 nodes). Add a 600-node graph through
streamAllCSVsToDisk asserting all rows land exactly once across the 500-row
flush boundary — no drops, dups, or corruption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(lbug): drop redundant step cast in buildRelRow (#2215 review)
GraphRelationship.step is already typed number?, so (rel as { step?: number }).step
was a no-op structural cast that obscured the shared-type coupling. Use rel.step
directly. Byte-identical — bench fingerprint unchanged, differential test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(lbug): make the unknown-label node drop explicit (#2215 review)
With the U3 `let pending` switch idiom, a node whose label matches neither
codeWriterMap nor multiLangWriters left `pending` undefined and was silently
dropped — a footgun for a future node type. Add an explicit else with a comment
documenting that unknown labels are intentionally not persisted and that a new
type must be wired into a writer map. No behavior change (byte-identity + tests
unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(lbug): drop the unused WriteStreamFactory re-export (#2215 review)
The type was re-exported from lbug-adapter 'to preserve this module's surface,'
but no external code imports it by name from here (the only test reference is a
comment). Keep the import from rel-pair-routing.ts (its canonical home, still
used by splitRelCsvByLabelPair's signature) and drop the dead re-export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): retain dense reaching-defs as differential oracle + fuzz harness (#2201 U1)
* refactor(cfg): extract shared harvest/adjacency/sweep + swappable in-set computer (#2201 U2)
* perf(cfg): sparse change-driven reaching-defs solver + canonical truncation (#2201 U3,U4)
* perf(cfg): switch production reaching-defs to the sparse solver (#2201 U5)
* perf(cfg): true SSA-sparse reaching-defs solver with auto-dispatch (#2201 U3)
Replace the per-variable worklist (correct but no faster — it still walks
pass-through blocks per binding) with Cytron SSA: CHK dominators + dominance
frontiers + phi-placement + stack renaming over a synthetic entry, answering
block-entry reaching queries by walking the SSA def-use graph (SCC-condensed,
cycle-safe). Pass-through blocks carry the dominating def via the rename stack
and phi-nodes statically capture loop merges, so dense-bindings drops from
O(n^2) to O(n) (5-23x faster, asymptotic) and deep nests are depth-independent.
The sweep now queries a lazy reachingAt accessor with a sparse intra-block
overlay (no full per-block lattice copy). Production auto-dispatches: SSA for
looping functions >=16 blocks (where it pays off, incl. the deep nests the
dense ceiling used to truncate -> ceiling stops firing), dense elsewhere (small
/ loop-free functions, 1.0x — no regression). Throw-edge and unreachable-block
functions fall back to dense (byte-identical). Held byte-identical to the dense
oracle across a 300k-CFG (~1.2M-comparison) differential fuzz.
* test(cfg): R5 contrast — dense ceiling fires, SSA solver converges (#2201 U6)
* bench(cfg): deep-nest scenario + tighten dense-bindings rd budget 10->2 (#2201 U7)
dense-bindings rd_scaling drops 5.2->0.86 (SSA linear); budget tightened to 2.0.
New deep-nest scenario (N nested loops, one carried var) measures rd under the
production blocks×64 ceiling and asserts the SSA solver still COMPUTES full
facts (facts_large_min) where the dense worklist would truncate — the
ceiling-stops-firing acceptance. CFG fingerprints unchanged.
* docs(cfg): document SSA-sparse solver + resolve the WTO no-go note (#2201 U8)
* fix(review): apply autofix feedback (#2201)
- Close the production SSA-dispatcher fuzz-coverage gap: the generator's
maxBlocks=14 was below SSA_MIN_BLOCKS=16, so the auto-dispatcher's SSA branch
was never differentially fuzzed. Raise to 36, add a hadLargeLoop coverage
assertion + a back-edge-into-entry canonical CFG. Validated byte-identical on
100k random CFGs incl. >=16-block looping shapes via both entry points.
- Correct stale function JSDocs + @internal annotations (dispatch/fallback roles).
- Add an independent rd_all_computed bench gate (catches partial truncation).
- maxBlockVisits comment, SSA_MIN_BLOCKS calibration note, nx->next rename.
* fix(cfg): gate out-of-range binding indices to the dense fallback (#2201 review)
Tri-review (adversarial lane, reproduced) found the SSA path less tolerant than
the dense oracle it replaced: an out-of-range binding index in defs/uses/mayDefs
(a corrupted/stale durable store) crashed the nBindings-sized arrays
(defBlocks[v]/stacks[u]), where dense tolerated it as a Map key. The throw
escaped the unguarded taint/harvest call sites and lost a whole file's taint
layer. Add a malformed-input gate that falls back to the dense solver (which
handles any index), preserving byte-identity AND the graceful per-function
degradation. Add an OOB canonical CFG to the differential fuzz + a production-
entry no-throw unit test (the generator only ever emitted in-range indices, so
this divergent input was structurally invisible).
* perf(cfg): bound the SSA value-graph, fall back to dense when oversized (#2201 review R1)
maxFacts bounds fact materialization in sweepFacts, but nothing bounded the
SSA-sparse solver's φ/value-graph construction. A high-binding-density deep
loop routed to SSA (≥16 blocks + a reachable loop) builds an O(blocks×bindings)
value graph the dense path would have truncated at its maxBlockVisits ceiling
(~1.5 GB measured on a 3000-block × 300-binding function).
Cap the value graph: after φ-placement (where nodeKeys.length == the φ count,
the input-superlinear term) plus a 2×Σgen bound on the renaming nodes, fall
back to computeInSetsDense before paying for renaming + Tarjan SCC. The fallback
is byte-identical (dense is the equivalence oracle) and bounded (dense honors
maxBlockVisits). Mirrors the existing throw/unreachable/OOB-binding gates.
The ceiling is DEFAULT_MAX_SSA_VALUE_GRAPH_NODES (1e6 — far above any real or
benchmarked function; dense-bindings/deep-nest build <1e4), overridable per call
via ReachingDefsLimits.maxSsaValueGraphNodes. The new unit test makes the
otherwise-invisible routing flip observable by pairing the cap with a tight
maxBlockVisits (dense truncates, SSA computes). Equivalence fuzz unchanged
(byte-identical, 20k CFGs green); tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(cfg): alias single-source SCC reaching-sets in reachByScc (#2201 review R2)
The SCC-condensation pass built a fresh Set for every SCC and copied each
cross-SCC operand's reaching-set element-by-element — O(defs²) at wide-fan-in φ
merges (a φ over many predecessors, each carrying a large reaching-set).
Add an alias fast path: an SCC with no own leaf keys whose cross-SCC operands
all resolve to ONE source SCC has exactly that source's reaching-set, so share
it by reference instead of copying. This is the common shape (pass-through φ /
single-operand value node). The full union is still built when an SCC has own
keys or genuinely merges ≥2 distinct sources.
Safe to share: reachByScc sets are read-only after construction (operand SCCs
are numbered before s in Tarjan's reverse-topological order and are only
iterated), and contents are identical — set iteration order is irrelevant
because sweepFacts sorts each use's keys before emission (KTD6). Byte-identical
to the dense oracle (30k-CFG fuzz green); tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(cfg): fold the SSA reachability gate into the RPO pass (#2201 review R8)
computeInSetsSparse ran a standalone reachability BFS to gate unreachable-block
functions to the dense oracle, then immediately computed a reverse-post-order
over the synthetic-entry graph — two traversals of the same successor structure.
reversePostOrder now returns the reachability bitmap its DFS already builds, and
the sparse path reuses it for the unreachable-block gate (S→entry is S's only
edge, so reachX[b] for b<n is exactly "reachable from entry" — identical to the
removed BFS). One traversal instead of two on every SSA-dispatched function.
The dispatcher's hasReachableLoop pass is left in place: it decides SSA-vs-dense
BEFORE the solver is entered, and computeInSetsSparse must stay self-contained
(the equivalence fuzz drives it directly, bypassing the dispatcher), so the two
cannot share a traversal without coupling the InSetsComputer contract.
Routing and facts unchanged — byte-identical to the dense oracle (30k-CFG fuzz,
including unreachable-block shapes, green); tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(cfg): trim per-statement/per-use/per-block allocations (#2201 review R9)
Three transient allocations in the hot paths, all behavior-preserving:
- sweepFacts: replace the per-statement `new Set([...defs, ...mayDefs])` with a
direct `includes()` scan over the (1–3 element) def/mayDef arrays, guarded by a
cheap hasSelfDefs flag that short-circuits pure-use statements.
- sweepFacts: reuse a single scratch array for each use's reaching def-keys
instead of spreading a fresh array per use. The KTD6 pre-sort still runs in
place (load-bearing for truncated byte-identity).
- computeInSetsSparse: build dPredsX by skipping consecutive-equal `from` values
(preds[b] is pre-sorted by buildAdjacency, so duplicates are adjacent) instead
of a per-block Set + spread + sort; the synthetic entry S = n exceeds every
block index so it appends in order.
The sweep is shared with the dense oracle, so these stay byte-identical on both
paths — 50k-CFG fuzz (incl. maxFacts truncation, the order-sensitive case)
green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cfg): correct the sweepFacts truncation byte-identity mechanism (#2201 review R6)
The outer sweepFacts JSDoc attributed a truncated result's cross-solver
byte-identity to the two solvers producing "identical inSets — insertion order
included". That is wrong: the dense (RPO fixpoint) and SSA (renaming/SCC)
solvers deliberately build a loop-carried use's reaching set in DIFFERENT
insertion orders — same set, different order. The actual mechanism is the KTD6
per-use sort that canonicalizes each use's keys by defKey BEFORE the maxFacts
cutoff (already documented correctly on the inner comment). Rewrite the outer
doc to say so. Documentation only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): extract pure graph sub-stages to reaching-defs-graph.ts (#2201 review R4)
reaching-defs.ts had grown to ~1190 lines with the #2201 SSA rewrite. Move the
self-contained, pure (plain-array) algorithms into a sibling module:
- reversePostOrder
- buildDominators (Cooper-Harvey-Kennedy)
- buildDominanceFrontiers (Cytron)
- tarjanScc + condenseReachingSets (SCC condensation, alias fast path)
- hasReachableLoop (dispatcher loop check)
- unionSets / latticeEquals (def-set / lattice primitives)
The new module has a STRICT one-way dependency (it imports nothing from
reaching-defs.ts — every helper is parameterized over plain arrays/Sets), so
there is no import cycle and each stage is independently testable. reaching-defs.ts
now holds the orchestrator, the two solver bodies, harvest, adjacency, the
statement sweep, and the dispatcher: 1190 → 988 lines.
Pure mechanical extraction — behavior is preserved by the differential
equivalence fuzz (40k CFGs byte-identical) + the reaching-defs unit/snapshot
suites; tsc clean. The helpers are @internal (kept out of the shipped .d.ts by
the stripInternal change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pdg): stamp the reaching-defs solver identity for incremental re-analysis (#2201 review R3)
The SSA-sparse rewrite computes full REACHING_DEF facts for deep-loop functions
the old dense worklist truncated to empty at the blocks×64 ceiling. But an
existing `--pdg` index carries those stale-truncated rows, and nothing forced a
re-analysis: RepoMeta.pdg had no solver-identity key, so an upgraded run over an
unchanged file kept the incremental fast path and never recomputed.
Add a constant `reachingDefSolver: 'ssa-sparse-v1'` to the resolved pdg stamp
(and to the RepoMeta['pdg'] type). It rides the existing key-union
pdgModeMismatch comparator: a pre-#2201 stamp lacks the key, so
'ssa-sparse-v1' !== undefined trips one full writeback that recomputes the
fuller coverage — no `--force` needed — exactly like the M2 REACHING_DEF cap and
M5 CDG cap upgrade paths. A matching post-#2201 stamp compares equal, so there
is no spurious re-analysis churn on steady-state re-runs.
Tests: new pre-#2201→SSA upgrade block in pdg-mode-flip.test.ts (stamp present,
absent-key mismatch, identical-stamp no-churn) + the persisted-stamp shape
assertions and resolvePdgConfig DEFAULTS updated for the new key. tsc clean;
pdg-mode-flip + run-analyze suites green (55/55).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(ts): stripInternal so @internal test-only exports stay out of the shipped .d.ts (#2201 review R5)
computeReachingDefsDense/computeReachingDefsSparse are exported only for the
equivalence fuzz and tagged @internal, but `declaration: true` emitted them into
the public dist/**/*.d.ts. stripInternal removes any @internal-tagged export from
the declaration output.
This is repo-wide, which is the intended behavior: the same applies to every
other test-only @internal export (hf-env's withDownloadTimeout etc., worker-pool's
buildDispatchMessage/crashSignature, parse-impl's handleWorkerStartupFailure, the
logger/safe-parse test resets, and the new reaching-defs-graph SSA helpers) — all
of which are documented as not-public.
Verified:
- declaration emit succeeds with no TS4094/TS9006 ("cannot be named") errors;
- the @internal functions are gone from the emitted .d.ts (reaching-defs-graph.d.ts
is now `export {};`), while public symbols (computeReachingDefs) remain;
- gitnexus-web — the only cross-package consumer — typechecks clean and imports
only from gitnexus-shared, never from gitnexus internals;
- runtime .js and the vitest/tsx tests are source-based, so unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(bench): add wide-merge scenario + tighten deep-nest facts floor (#2201 review R7)
wide-merge: N bindings, each assigned in a 3-way branch (a wide multi-operand φ
per binding) inside a loop, then all used after the merge. Unlike dense-bindings
(one chained redef per `if`), every binding fans into its own wide φ, so the
scenario exercises φ-placement + renaming + the reachByScc condensation across
many independent wide merges. N bindings × constant arms ⇒ O(N) facts, so the
gate is rd_scaling LINEARITY (measured ~1.07; budget 2.0 catches a regression to
the per-binding-rescan O(N²) class the reachByScc alias path guards against). It
runs the production SSA path (10007 blocks + a loop) and computes all facts under
the blocks×64 budget (facts_large_min 24000 of a measured 26008 + the
rd_all_computed gate).
deep-nest: tighten facts_large_min 100 → 150 (measured 164) so a partial-
truncation regression that still cleared 100 — but lost facts — now fails, with
~9% headroom for noise.
bench --check PASS (9 scenarios) under --expose-gc; all existing CFG fingerprints
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(cfg): drop trailing blank line in reaching-defs.ts (prettier)
Whitespace-only — a stray trailing newline left by the U4 extraction. `prettier
--check` (the root format CI gate) now passes on every changed file. No behavior
change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>