regression-audit-2026-09.md 27 KB

Release-to-main correctness repairs (September 2026)

Compared the installed @colbymchenry/codegraph@1.6.0 npm bundle (release dfccdf62547fcd76d343344d823a0e1998d3a89f) with main 3ed73bc127323e63153bf6ec8354afa82ce36aaf. Both ran with the bundle's Node 24.16.0 on Linux x64, identical fixture revisions/settings and separate indexes. Native and forced-WASM probes were kept separate. Main was fetched again before this change; the comparison base had not advanced.

Confirmed losses and repairs

Loss Introducing change Repair
Typed TSX-to-TS field calls, including Excalidraw's observer registration and mutation-to-render flow cece072 (#1792) Use the same JS/TS language family in cached and uncached method lookup.
Destructured Zustand actions lose their callers cd4e65b (#1759) Trace the actual state binding before rejecting locally bound names.
Adding interface signatures makes store accessor calls ambiguous ee83636 (#1780) Find the implementation inside the identified store, not a globally unique name.
Direct React Native bridge calls disappear de5adba (#1790) Retain qualified call sites and let the framework validate the module.
Dart extension-type getters disappear in WASM ee83636 (#1780) Apply the bodyless-signature guard only to its intended JS/TS grammars.

Each introducing commit was checked against its parent with the same minimal fixture. The lost relationships were checked against source wiring rather than inferred from edge-count differences. The Excalidraw path is Scene.mutateElement → Scene.triggerUpdate → App.triggerRender → App.render → StaticCanvas → renderStaticScene.

Remaining suite failures

The nine failing Steps assertions had two causes: external member-chain call sites had been discarded before effect classification, and valid Zustand selector bindings were blocked as opaque local calls. Qualified external references now survive without becoming guessed internal call edges. Store selectors require a Zustand factory import, resolve the selected member in that store, and respect lexical scope and shadowing. Renamed selections and closure captures are covered, including negative cases for unrelated factories, stores, parameters, and local declarations.

The tenth failure was a stale callers-truncation fixture: it counted a filename as an overload of its exact-named function. The fixture now contains two real functions and checks that both truncated sections carry their markers. The extraction parity expectations now assert the exact retained qualified references and all argument calls. No assertion was removed or replaced by a skip.

Validation

  • All ten formerly failing assertions pass on native and forced WASM.
  • The final forced-WASM run passes 148 tests across eleven affected suites.
  • Native resolver, framework, graph, context, sync-convergence, and explore budget checks pass. The final expanded guard/parity run passes 94 tests.
  • All 655 extraction tests pass in seven sequential fresh-process batches; the union of passing test names is checked against all 655 original cases.
  • Native/WASM TS/JS parity passes for the torture fixtures, CRLF forms, real source files, and optional/ordinary member chains. Dart parity also passes.
  • The C deep-brace guard, shallow-file checks, worker checks, and built CLI stress checks pass. The two all-language 60,000-level cases remain outside the completed stress result, as explained below.

The original fresh-index corpus checks cover Express, Gin, Django and Excalidraw. Source-grounded paths pass 11/12 on release, 9/12 on original main, and 11/12 after repair. The remaining Django compiler path is absent in both original versions. Original integrity, foreign-key and orphan checks pass on all 44 retained corpus indexes; no-op sync preserves all fingerprinted edge sets. Existing Gin and Django edit/restore drift is not repaired by this change.

Runtime limitations

The monolithic extraction suite's worker receives SIGKILL as its resident memory grows to roughly 1.6–1.7 GB; sampled JS heap use is only 70–95 MB. Dense C++ fixtures and subsequent indexing setup produce substantial native memory growth. Lowering the JS heap or worker count does not resolve it. Fresh batches keep peak child RSS below approximately 0.8 GB and run every assertion successfully. This distinguishes the resource/lifetime sensitivity from the ten reproducible assertion failures, but does not identify the exact native allocation-retention cause.

Standalone Scala expressions nested 60,000 levels did not finish within a 45-second diagnostic budget in either native or WASM; equally deep block expressions did not avoid the parser limitation. The existing stress tests are unchanged. There is no claim of a full green monolithic suite or a repair of this extreme-input parsing limit.

These checks do not cover other operating systems, a long-running watcher, MCP transport latency, or the paid agent A/B harness. Shared-host timing was noisy, particularly for Excalidraw, and is not used to claim a performance win.

Independent-review follow-up (September 14–15, 2026)

The user-supplied Claude review of this PR reported 4,636 passing / 3 failing tests, compared with 4,600 / 18 before the repairs, and confirmed the five correctness fixes. Those full-suite figures are independent review evidence; this follow-up did not repeat the completed full audit.

The two CLAUDE_CONFIG_DIR failures were path-alias mismatches. The test fixtures now canonicalize their temporary home and working directories with realpathSync, matching chdir's behavior on macOS /var/private/var. The exact path, file content and idempotency assertions remain in place. Both failures were reproduced on Linux using a symlinked TMPDIR before the change; all seven override cases pass afterward. macOS was unavailable, so this is a reproduced path-alias fix, not a claim of a macOS test run.

The PPID watchdog integration test now launches its wrapper and descendants in its own temporary project. An editor's writer lock in the source checkout can no longer end the child before the watchdog is exercised. The existing assertions still require a live child, held-open stdin, detection of its terminated parent and actual child shutdown. No live lock was changed and no user server was stopped. The Linux check used a subprocess subreaper to provide the orphan-reaping behavior otherwise supplied by docker --init.

The direct name-matcherimport-resolver cycle is removed. The resolver coordinator supplies import lookup through ResolutionContext, preserving the existing import resolver and its caches. Two additional exact-target-set tests cover store actions through a barrel re-export and renamed import, including exclusion of another store's identically named action.

Follow-up validation:

  • 233 native resolver/regression cases and 42 affected WASM cases pass.
  • The installer suite passes 245 cases with its three existing platform skips; the separate symlink-root reproduction passes all seven selected override cases.
  • The real PPID process case and 14 watchdog decision cases pass.
  • npm run build passes, including the viewer and packaged grammar checks.
  • The monolithic-worker and extreme Scala-input limitations above remain; these test-isolation changes do not repair native allocation retention.

Bounded large TypeScript comparison

The comparison indexes microsoft/vscode's src/vs/platform subtree at 38246c086c8a825ca90190749dd88df6effec257: 2,623 TypeScript files (26.7 MiB of TypeScript source), plus 12 JavaScript and 457 YAML files. This is a large subsystem, not all of VS Code; definitions outside the subtree are absent in both arms. It is larger than the previously pinned Excalidraw fixture.

The baseline is main 3ed73bc127323e63153bf6ec8354afa82ce36aaf; the fixed arm is PR head c7d2892180874f42b9f9f99119f2868fe093a816 plus the six-file follow-up committed locally as ec13d99. This measures the whole PR versus its base, not the isolated causal cost of qualified-chain retention or the cycle refactor. Both builds use the bundled Node 24.16.0 and identical source.

Three sequential pairs were attempted per backend, with baseline/fixed order reversed for the middle pair. Each run has a fresh process and database, one assigned CPU, one parse worker, one resolver worker, parallel resolution disabled, RAYON_NUM_THREADS=1, a 1 GiB JS heap limit, and --liftoff-only. Source pages are warmed once before the first pair; there is no separate discarded warm-up run. No other audit test/build runs concurrently. Limits are 150 seconds and 1,500 MiB sampled RSS per process. Successful native runs consumed about 9.8 minutes; the WASM continuation was capped at 10 minutes, keeping benchmark subprocess time below 20 minutes.

Three native pairs and two WASM pairs completed. The third WASM baseline also completed (111.7 seconds), but its fixed partner was stopped after 21.6 seconds when the continuation budget expired. That pair is excluded from comparisons; the interrupted run is not a product failure or a valid timing result.

The initial three WASM preflight attempts were rejected by a harness mistake: getKernel() checks whether the native library is installed but deliberately ignores the kill switch. The guard was corrected to kernelSupports('typescript'), the extraction routing predicate. Those attempts performed no indexing and are preserved but excluded. Completed native runs were not repeated.

Total process wall time includes startup, indexing, database checks and close. Index wall/CPU time brackets indexAll(). The resolution stage wraps resolveReferencesBatched and includes persistence and synthesis as well as matching. Process CPU time includes its worker threads; RSS is sampled every 100 ms and checked against the process high-water mark. These controls reduce local contention but cannot reserve the shared host's CPU.

Values are median (minimum–maximum) over complete pairs only. The last column is the median of the per-pair percentage changes, not a ratio of the two displayed medians. Positive values mean more time or memory.

Backend / metric Main baseline Fixed PR Paired change
native / Process elapsed (s) 92.0 (91.2–95.2) 102.5 (101.6–102.8) +11.3%
native / Index elapsed (s) 87.8 (87.2–91.1) 97.2 (96.4–98.7) +10.6%
native / Index CPU (s) 54.5 (53.9–55.9) 60.8 (60.1–61.5) +10.2%
native / Resolution elapsed (s) 50.1 (39.0–52.1) 59.5 (57.9–61.3) +17.7%
native / Resolution CPU (s) 37.6 (37.1–38.8) 43.8 (42.8–44.1) +13.8%
native / Process peak RSS (MiB) 1235.7 (1168.9–1250.8) 1194.3 (1149.1–1196.3) -3.3%
native / Resolution peak RSS (MiB) 1218.7 (1141.4–1230.5) 1170.9 (1148.0–1181.4) -3.1%
wasm / Process elapsed (s) 112.4 (111.3–113.4) 121.0 (117.2–124.8) +7.7%
wasm / Index elapsed (s) 108.2 (107.2–109.3) 116.3 (113.0–119.6) +7.5%
wasm / Index CPU (s) 77.3 (77.1–77.4) 83.3 (82.7–83.9) +7.7%
wasm / Resolution elapsed (s) 49.7 (45.3–54.0) 56.5 (51.6–61.4) +15.6%
wasm / Resolution CPU (s) 37.8 (37.7–37.8) 43.7 (43.5–43.9) +15.7%
wasm / Process peak RSS (MiB) 1163.4 (1119.2–1207.6) 1127.4 (1051.8–1203.0) -2.7%
wasm / Resolution peak RSS (MiB) 1135.8 (1091.5–1180.0) 1111.2 (1041.4–1181.1) -1.8%

The completed pairs show a consistent increase in CPU work: about 10.2% native / 7.7% WASM for indexing and 13.8% / 15.7% for the resolution stage, using median paired changes. Whole-process elapsed time increases by 11.3% / 7.7% here. Resolution wall time is much less stable (one WASM pair actually decreases), so an exact wall-time penalty is not portable to another host. Memory ranges overlap and pairwise RSS changes have both signs; this does not establish a memory improvement or regression. The prior small-corpus 3.4-second observation does not establish that the added work is free at scale.

All 11 completed indexes pass integrity, foreign-key and orphan checks with zero indexing errors. Each contains 75,767 nodes and 256,523 edges. Pending/failed references after indexing increase from 156,817 to 166,516 (+9,699, 6.2%); references entering resolution increase from 342,370 to 352,069 (2.8%). The additional retained references do not create guessed internal calls. Complete row/multiplicity comparisons of the first pair in each backend also confirm identical node and edge contents, excluding node update timestamps and auto-increment row IDs. All 9,699 additions are qualified call references; no unresolved reference was removed. Bounded-memory SQL was used after an initial in-memory postprocessing attempt was interrupted; the index runs and saved databases were unaffected.

The precision and correctness fixes remain warranted. This experiment finds a bounded, repeatable CPU cost on this large subsystem, not an isolated causal estimate for one retention rule and not a full-VS-Code/default-worker benchmark. There is no new timing-based release gate or claim of unchanged performance. The unavailable macOS run and earlier parser/worker limitations remain explicit.

Reproduction and retained evidence

The portable scripts/benchmarks/measure-index.cjs harness accepts a built engine directory, fixture directory, existing output directory and backend. Build both pinned engines first and stage their matching native kernels. Set BENCH_NODE, BENCH_ENGINE, BENCH_FIXTURE, and BENCH_OUT to absolute paths; use a new output directory and an unindexed fixture for each run. For WASM:

mkdir -p "$BENCH_OUT"
test ! -e "$BENCH_FIXTURE/.codegraph"
CODEGRAPH_TELEMETRY=0 DO_NOT_TRACK=1 CODEGRAPH_NO_UPDATE_CHECK=1 \
CODEGRAPH_KERNEL=0 CODEGRAPH_WASM_RELAUNCHED=1 \
CODEGRAPH_PARSE_WORKERS=1 CODEGRAPH_RESOLVE_WORKERS=1 \
CODEGRAPH_NO_PARALLEL_RESOLVE=1 RAYON_NUM_THREADS=1 \
taskset -c 0 "$BENCH_NODE" --liftoff-only --max-old-space-size=1024 \
  scripts/benchmarks/measure-index.cjs \
  "$BENCH_ENGINE" "$BENCH_FIXTURE" "$BENCH_OUT" wasm
mv "$BENCH_FIXTURE/.codegraph" "$BENCH_OUT/index"

For native, change CODEGRAPH_KERNEL=1 and the final argument to native. Use an available CPU from the host's affinity mask. Repeat sequentially in baseline/fixed, fixed/baseline, baseline/fixed order. The outer runner enforces the stated time/RSS ceilings, samples RSS, and preserves every database.

The full commands, outer runners, source/build fingerprints, every attempted run, RSS samples and final JSON summaries are retained under /data/workspace/codegraph-regression/review-followup/. artifacts/perf-summary.json contains the complete-pair statistics; artifacts/perf-manifest.json records all attempts, including the rejected preflights and interrupted last run. The original audit artifacts and indexes remain unchanged.

Store eligibility cache follow-up (2026-09-15)

The per-file .getState gate in matchDestructuredStoreCall now caches both boolean answers per resolver context, capped at 8,192 files with FIFO eviction. It is cleared with source caches during sync. Positive files still run all existing lexical, shadow, import and store-action checks. Qualified references and both extractors are unchanged; no diagnostic helper bypass was applied.

The pre-optimization PR is c6036f09fb1af3c5f4ae680d4ca63a0978016871; implementation is 92a6c85de7050e99034a12bea1494375f5cbdab8. Compiled-engine fingerprints show only name-matcher.js changed, with an identical native kernel.

Three new real-SQLite tests pass on native and WASM: same-instance edit+sync in both directions, independent projects sharing relative paths, rejection of a same-named decoy, and retained qualified call coordinates/multiplicity. Deliberately removing invalidation makes the negative-to-positive case fail; that mutation was restored. Native focused checks passed 234/236 at normal limits; two Objective-C cases timed out at five seconds (also in isolation), then all four Objective-C assertions passed with a diagnostic 15-second allowance. The baseline four passed in 0.66 seconds; the optimized diagnostic run spent 75 seconds collecting tests. This is not a default-limit native-suite green verdict. WASM verified 45 affected cases: 32 initially plus 13 Steps cases passing in isolation after a combined-run setup timeout. No committed timeout or assertion was weakened. TypeScript/assets passed; the viewer build reached the first 240s bound, then completed separately with all 29 grammar asset checks.

Full native before/after indexes on saved Excalidraw afa3a653 have identical node, edge and retained-reference contents and multiplicities, excluding only update timestamps and auto-increment IDs:693 files,12,779 nodes,54,045 edges,38,044 references. Integrity/FK/orphan/error checks pass. This preserves the repaired render/store relationships and the source evidence.

Fresh VS Code platform timings were bounded to two reversed-order native pairs on the previously pinned corpus, same physical root, one CPU and worker, Node 24.16.0,1GiB heap/1,500MiB RSS. Three attempts reached 180s before resolution completed; the repeated-limit stop rule cancelled the fourth. No whole-index speedup is established. The correctness-only Excalidraw pair also varied: index CPU 11.90→15.87s, resolution CPU 7.71→9.57s, elapsed 14.45→170.89s, peak RSS 558.9→551.3MiB. Even CPU outside the changed stage rose 4.19→6.31s. This single pair cannot isolate a cache-caused improvement or slowdown.

A bounded diagnostic replay through the actual store matcher and production contexts, with all 207,446 saved JS/TS call references, fresh contexts and reversed order, confirms the direct benefit without bypassing any helper:

Pair Before helper CPU Cached helper CPU Reduction
Before then cached 7.265s 1.046s 85.6%
Cached then before 6.768s 0.990s 85.4%

Both return identical results (zero matches on this corpus). The replay's broader call set differs from the actual pipeline invocation set and excludes other resolver work, extraction, persistence and synthesis. Its 5.8–6.2s saving is not a whole-index estimate and does not establish that the earlier 8–10% penalty or diagnostic 4.7s has been recovered in full. WASM timings and the full audit matrix were not repeated. Earlier Mac/parser/worker limitations remain.

Commands, the preserved pre-change engine, scripts, all attempts, databases, checks and full SQL comparisons are in /data/workspace/codegraph-regression/review-followup/cache-optimization/; REPORT.md and artifacts/summary.json consolidate the evidence.

Completing the large benchmark and explaining the timeouts (2026-09-15)

All four fresh native indexes and their full database checks completed. The old three stops were SIGTERM from the audit runner's 180-second wall timer, not CodeGraph rejecting a large project or running out of memory. Those interrupted artifacts are unchanged. They lack CPU/progress traces, so their precise wait sites cannot be reconstructed retrospectively.

The completing comparison uses the same VS Code platform tree (38246c086c8a825ca90190749dd88df6effec257, source fingerprint 3d629a40f93a90d29d5aa00bd06f2a7e119b4d628f20bb449cd815d972e4ccdb), 3,092 supported files, Node 24.16.0, native parsing and fresh SQLite databases. Pre-cache engine: c6036f09fb1af3c5f4ae680d4ca63a0978016871; cached engine: da5e6e76c908447d0abd3e6c05e11deb64984736. Only compiled name-matcher.js differs; the native kernel is identical. All qualified-reference retention and the repaired matching guards are preserved.

Runs were sequential, cached/before with the old one-core restrictions, then before/cached with both available CPUs and automatic parser/resolver sizing. The latter is normal CPU configuration; both arms still use the same 1GiB V8 heap cap and --liftoff-only. Automatic resolution correctly stays sequential on this two-CPU VM. Both sides use identical lightweight phase/batch/DB-call observers; no V8 sampling profiler, reference bypass, or runtime code edit. There was no elapsed-time termination condition. Each child was polled until completion, with progress, CPU, RSS, thread scheduler/wait state, pressure, I/O and cgroup counters saved. No other servers or locks were touched.

CPU configuration Engine Index elapsed Index CPU Resolution elapsed Resolution CPU Peak process RSS
One core, forced sequential Before cache 144.19s 61.33s 89.51s 44.15s 1003.8MiB
One core, forced sequential Cached 133.86s 57.58s 79.20s 40.43s 1031.4MiB
Two cores, automatic workers Before cache 126.31s 62.73s 74.97s 44.84s 1006.9MiB
Two cores, automatic workers Cached 128.05s 58.21s 77.39s 39.98s 1037.5MiB

Index times cover await cg.indexAll(), including maintenance. Resolution includes setup, matching, persistence and synthesis. Full processes, including subsequent integrity/FK/orphan scans and shutdown, took 176.70/191.21/178.91/191.50s in execution order. In the new baseline attempts, a 180s process limit would have confused an already completed index with an unfinished verification.

The cache saves 3.75–4.52s of whole-index CPU (6.1–7.2%) in these pairs. Matching CPU falls 30.62→26.69s and 31.14→26.43s; resolution CPU falls 8.4–10.8%. This supports a real CPU benefit from the narrow cache. It does not establish a universal wall-time improvement or that every part of the original 8–10% CPU increase is recovered: one elapsed comparison improves 7.2%, the other worsens 1.4%, and these are only two pairs across two CPU configurations.

What caused the long waits

The reproduced delays are predominantly disk/page waits under memory and I/O pressure, not a matching loop that gets progressively more expensive:

  • Main-thread samples repeatedly show D state in folio_wait_bit_common, rq_qos_wait, buffer/journal waits and block-request allocation. These are kernel storage/page waits. Index maintenance has an idle, responsive main event loop while its worker completes I/O; no resolver pool deadlock appears.
  • Global I/O pressure reports all runnable work stalled for 57.5–63.8% of the sampled whole-process windows. This is a host metric, not an exact per-stage allocation, but the indexer's own wait states directly corroborate it.
  • The VM has 3,916.6MiB total RAM. In the three runs with continuous meminfo capture, available memory reaches only 158.0, 142.2 and 92.0MiB. A spot check during the first completing run showed about 262MiB available. Memory-pressure counters also rise. The exact source of shared memory/storage pressure is not identified; no unrelated processes were modified.
  • Visible CPU quota is unlimited; throttling counters remain zero. CPU steal is only 0.21–0.30% over these runs. CPU starvation is not the dominant observed delay. Thread CPU/scheduler samples and responsive maintenance heartbeats distinguish CPU work from waiting.
  • Across four successive groups of 18 matching batches, cached one-core median CPU per batch is 365, 353, 268 and 269ms. In the two-core cached run it is 365, 329, 270 and 253ms. CPU work does not grow with progress. Late elapsed batches can stretch while CPU remains low because the process waits for pages.
  • Setup is only 0.06–0.07s. Matching, SQLite inserts/cleanup, index rebuilding, synthesis and final maintenance have separate observations. For example, one-core cached matching takes 45.29s elapsed but 26.69s CPU; synthesis takes 15.26s elapsed/6.94s CPU, and final maintenance takes 25.04s elapsed.

Three diagnostic reports were requested during long final verification gaps; they were delivered when the synchronous work yielded, so their JS stacks are empty and are not used as hotspot evidence. Kernel wait samples, batch CPU and phase logs are the actionable evidence. No healthy process was killed.

Correctness and benchmark repair

All four runs have exactly the same 75,767 nodes, 256,523 edges and 166,516 retained references, including 89,157 qualified names. Every retained reference has been processed (failed denotes unresolved after attempted matching); zero remain pending. Full SQL EXCEPT comparisons in both directions, grouping complete rows with multiplicity, show zero additions/removals. Only node update timestamps and auto-increment edge/reference IDs are excluded. Ordered SHA-256 fingerprints also match for all three tables. Integrity checks are ok, with no foreign-key violations, orphan edges, indexing errors or missing supported files.

No further resolver change was warranted by this evidence. The benchmark now writes phase/batch progress, cumulative CPU/RSS and event-loop measurements as it runs, names maintenance separately, and saves index-result.json before full verification. result.json represents completion of checks and shutdown. Database verification failures produce nonzero exit status. It refuses existing fixture indexes and reused trace files, preserving prior evidence.

The portable Linux observer has no wall timeout, records the child/thread/host resource counters every two seconds, and leaves the caller's CPU/environment settings unchanged. For example, from a built checkout (all engine/fixture/output paths absolute; the output directory must not exist):

CODEGRAPH_KERNEL=1 CODEGRAPH_TELEMETRY=0 DO_NOT_TRACK=1 CODEGRAPH_NO_UPDATE_CHECK=1 \
CODEGRAPH_WASM_RELAUNCHED=1 python3 scripts/benchmarks/observe-index.py /absolute/run-before -- \
  /absolute/node --liftoff-only --max-old-space-size=1024 \
  scripts/benchmarks/measure-index.cjs /absolute/built-before /absolute/pinned-fixture \
  /absolute/run-before native

Watch progress.ndjson and resources.ndjson; inspect CPU deltas, thread wait states and phase progress before stopping an apparently slow child. Archive that run's owned .codegraph directory before the next fresh run. Use the same source, flags and observer on both sides; run sequentially. Do not treat an external execution deadline as a product failure or compare incomplete databases.

The revised harness and observer passed actual native and WASM integration checks on a two-file fixture: the real cross-file call and retained qualified external reference exist; completed-index evidence precedes verification; maintenance is identified; successful observer exits are recorded; and refusal of an existing index leaves its SQLite bytes unchanged. JavaScript syntax, Python compilation and diff checks pass. Product code and compiled engines are unchanged by this follow-up, so prior focused resolver tests remain applicable; no full-suite rerun, Mac validation, or new npm release is claimed.

Full commands, four complete databases, raw observations, diagnostic startup failure (a worker inherited the preload; fixed before the four measured runs), reports, graph comparisons and harness checks are retained in /data/workspace/codegraph-regression/review-followup/timeout-diagnosis/. artifacts/summary.json and artifacts/provenance.json consolidate the evidence.