Commit 5c9683e
authored
fix: align enclosing-caller attribution for variable bindings (haskell, zig) (#1499)
* chore: gitignore napi-generated artifacts in crates/codegraph-core
* chore(tests): remove unused biome suppression in visitor.test.ts
* fix(titan-run): sync --start-from enum and phase-timestamp list with actual phases
* fix(hooks): track Bash file modifications via before/after git status diff
Adds snapshot-pre-bash.sh (PreToolUse Bash) + track-bash-writes.sh
(PostToolUse Bash): the pre-hook captures git status --porcelain to a
per-worktree temp file before each Bash call; the post-hook diffs the
before/after state and appends newly modified or created files to
.claude/session-edits.log.
This closes the gap where files written by sed -i, printf redirects,
tee, heredocs, or build tools (Cargo.lock, lockfiles) were never
recorded, causing guard-git.sh to emit false-positive BLOCKED errors.
Closes #1457
* chore(native): remove dead code (unused var, method, variant, fields)
- clojure.rs: annotate lifetime-anchor assignment to silence false-positive
- cfg.rs: remove never-called start_line_of method
- complexity.rs: remove never-constructed NotHandled variant; convert
irrefutable if-let patterns to plain let destructures
- dataflow.rs: remove never-read callee fields from CallReturn/Destructured
- incremental.rs: remove never-read lang field from CacheEntry
cargo check and cargo clippy both clean after these changes.
* refactor(native): extract emit_pts_alias_edges params into PtsAliasCtx struct
* fix(wasm): sort call targets by confidence before emit to match native engine
* fix(bench): add 2 warmup runs and raise INCREMENTAL_RUNS to 5 for incremental tiers
* ci(bench): add per-PR perf canary for extractor/graph/native changes
Adds .github/workflows/perf-canary.yml — a path-filtered workflow that
fires on PRs touching src/extractors/, src/domain/graph/, or crates/**
and runs only the incremental-benchmark suite (full build + no-op +
1-file rebuild, both engines). Catches the class of regressions that
accumulated invisibly across the Phase 8.x PRs and were only detected
at v3.12.0 publish time.
The regression guard gains BENCH_CANARY=1 mode: raises thresholds to
50%/100%/150% (standard/noisy/WASM) and skips the build, query, and
resolution suites — only incremental checks run. This absorbs shared-
runner timing variance while still blocking catastrophic regressions
(+98% full build, +1827% 1-file rebuild from v3.12.0).
Closes #1433
* fix(perf): plumb symbolsOnly through parseFilesWasmInline to skip analysis visitors
* fix(perf): scope runPostNativeCha to changed files on incremental builds
On incremental builds, runPostNativeCha previously scanned all
call→qualified-method edges in the DB (~12ms flat, O(graph size)),
even for 1-file changes where no hierarchy or RTA evidence changed.
Add two cheap indexed gate queries. Gate A checks whether any changed
file introduced a class/interface/trait/struct/record node (hierarchy
may have new implementors reachable from unchanged call sites). Gate B
checks whether any changed file added a call edge to a class-kind target
(RTA set may have grown, enabling previously filtered expansions in
unchanged callers). If neither gate fires, restrict the candidate query
to src.file IN changedFiles — safe because the hierarchy and instantiated
set are unchanged for all other files.
Full builds (isFullBuild=true) and cases where either gate fires retain
the existing full-scan behaviour. Mirrors the changed-files scoping
pattern of runPostNativeThisDispatch.
Closes #1441
* fix(native): add post-pass phase timings to result.phases
Times each JS post-pass in tryNativeOrchestrator and exposes the
measurements in BuildResult.phases:
- gapDetectMs — dropped-language gap detection + backfill
- chaMs — CHA expansion (interface dispatch)
- thisDispatchMs — this/super dispatch WASM re-parse (was already
tracked but now properly named alongside the rest)
- reclassifyMs — scoped role re-classification after edge insertion
- techniqueBackfillMs — technique-column UPDATE on native-written edges
Previously only thisDispatchMs was reported, causing wall-clock vs
phaseSum to diverge by 1.1s+ on 1-file rebuilds and making benchmark
regressions undiagnosable from committed history.
Updates update-incremental-report.ts to render the new phases in a
collapsible details block under each engine's 1-file rebuild section.
Closes #1434
* fix(perf): correct INLINE_BACKFILL_THRESHOLD docstring; raise threshold for required-tier grammars
The docstring claimed pool cost was "amortised over enough parse work" —
measurements show IPC overhead scales linearly (~55–64ms/file pool vs
~8–10ms/file inline). The real motivation is crash safety for exotic WASM
grammars (#965); JS/TS/TSX (required-tier, used in all this-dispatch
backfill calls) have never triggered the V8 fatal crash class and are safe
to run inline.
Raise threshold 16 → 32 to keep typical this-dispatch batches (≤ 18 files
on the codegraph corpus) on the inline fast path. Exotic-language drops are
almost always well under 32 files and also benefit from the inline path
without meaningful crash risk increase.
Closes #1435
* fix(perf): guard post-native passes against unnecessary work on 1-file incremental rebuilds
On 1-file native incremental builds, two JS post-passes ran unconditionally
even when they had no work to do:
- `backfillNativeDroppedFiles`: called whenever changedCount > 0, even when
detectDroppedLanguageGap returned an empty gap. Gate now checks
gap.missingAbs.length > 0 || gap.staleRel.length > 0 directly, matching
backfillNativeDroppedFiles's own internal early-exit guard.
- Node/edge COUNT(*) re-count: ran unconditionally after all post-passes even
when none of them wrote any edges. COUNT(*) over 50K+ edge tables is
non-trivial, especially via the NativeDbProxy napi-rs round-trip. Now gated
on postPassWroteData (backfill | CHA edges | this-dispatch edges).
Closes #1454
* chore(types): remove dead protoMethodsMs field and stale comment
The post-pass it timed (runPostNativePrototypeMethods) was deleted in
b5c03a2 when func-prop extraction moved to Rust (#1432). The optional
field was never set by any code path that survived the deletion.
Also remove the stale reference to "prototype-methods post-pass" from
the parseFilesWasmForBackfill docstring — only the this-dispatch
post-pass uses symbolsOnly now.
Closes #1432
* fix: class-scope field annotation typeMap keys to prevent cross-class collision
Field type annotations (`private repo: OrderRepository`) were seeded as bare
file-wide typeMap keys, causing `this.repo` inside `UserService` to resolve to
`OrderRepository` when both classes had a `repo` field (issue #1458).
Both extractors (TS `handleFieldDefTypeMap` and Rust `field_definition` branch)
now seed `ClassName.field` keys at confidence 0.9, matching the `CallerClass.X`
resolver fallback added in PR #1382. Bare keys are kept at confidence 0.6 as
fallbacks for single-class files or class expressions where no enclosing class
name is available.
Both engines change identically — parity preserved.
* fix(bench): update elixir/julia/objc expected-edges to module-qualified names
The resolution benchmark uses WASM-built graphs where the Elixir, Julia,
and Objective-C extractors emit module-qualified symbol names (Main.run,
App.main, UserService.create_user, etc.). The expected-edges manifests
were written with bare unqualified names (run, main, create_user), so
every correctly-resolved edge appeared as a false positive and every
expected edge appeared as a false negative — causing all three languages
to show 0% precision even though resolution was working correctly.
Root cause: starting in v3.12.0, cross-module call resolution began working
for these languages (via the improved receiver-dispatch and same-class
fallback in resolveByMethodOrGlobal / build-edges.ts). With 0 edges
previously resolved, the name mismatch was invisible; once edges started
resolving, the manifests showed 17 FP (elixir), 11 FP (julia), 6 FP
(objc) — all correctly resolved edges misidentified as false positives.
Fix:
- Update all three expected-edges.json manifests to use the
module-qualified names matching actual extractor output:
elixir: Main.run, UserService.create_user, Validators.validate_user, etc.
julia: App.main, Service.create_user, Repository.new_repo, etc.
objc: full ObjC selectors (createUserWithId:name:email:, isValidEmail:, etc.)
plus add main -> run (plain C call correctly resolved)
- Ratchet THRESHOLDS for all three:
elixir: precision 0.0 -> 1.0, recall 0.0 -> 0.8 (17/21 resolved)
julia: precision 0.0 -> 1.0, recall 0.0 -> 0.7 (11/15 resolved)
objc: precision 0.0 -> 1.0, recall 0.0 -> 0.4 (6/13 resolved)
Remaining FNs are genuine unresolved edges (same-file bare calls in
elixir/julia, receiver-typed message sends in objc) — not regressions.
Closes #1447
* fix(wasm): emit receiver edges for declaration-typed locals in C++/CUDA
The JS C++ and CUDA extractors had no handler for 'declaration' AST nodes,
so typeMap was never seeded for statically-typed locals (e.g. 'UserService svc;').
Without a typeMap entry for 'svc', resolveReceiverEdge had nothing to look up and
silently skipped the receiver edge.
Add handleCppDeclaration / handleCudaDeclaration to both extractors. They mirror
match_c_family_type_map ('declaration' branch) from the native Rust path: extract
the type node text and seed typeMap[varName] = { type, confidence: 0.9 } for each
identifier or init_declarator child. Primitive types (int, char, bool, …) are
skipped to avoid spurious edges.
parity-compare.mjs --langs cpp,cuda --hybrid: PARITY OK (wasm = native = hybrid)
All 3044 tests pass.
* fix(native): resolve Go factory and Python constructor receiver types in Rust solver
Go extractor was only seeding typeMap for var_spec and parameter_declaration,
missing short_var_declaration. Added infer_short_var_types to handle:
- x := Struct{} → conf 1.0 (composite literal)
- x := &Struct{} → conf 1.0 (address-of composite)
- x := NewFoo() / x := pkg.NewFoo() → conf 0.7 (New* factory prefix)
Python extractor was only seeding typeMap for typed_parameter and
typed_default_parameter, missing plain assignment. Added
infer_py_assignment_type to handle:
- order = Order(...) → conf 1.0 (uppercase constructor)
- obj = Module.Class(...) → conf 0.7 (uppercase module prefix, non-builtin)
Both mirror the existing JS extractors exactly. Parity check for
go and python: wasm vs native/hybrid OK.
* fix: align enclosing-caller attribution for variable bindings (haskell, zig)
Both engines used different rules for attributing calls inside variable bindings:
WASM: attributed to the narrowest enclosing span regardless of kind, so
local variable declarations inside fn main() shadowed the enclosing function
(Zig: calls attributed to repo/svc variables instead of main), and nested
let-bindings inside a Haskell do-block shadowed the top-level main binding.
Native: loaded allNodes from a query that excluded 'variable' kind, so
top-level Haskell bind nodes (main = do …, kind='variable') never matched
in defs_with_ids, causing all calls to fall back to the file node.
Unified rule implemented in findCaller (TS) and find_enclosing_caller (Rust):
- Function/method definitions are preferred over any variable/constant binding
as the enclosing caller scope — local var declarations inside a function
body never shadow the enclosing function (fixes Zig repo/svc attribution).
- When no function/method encloses the call, fall back to the WIDEST
(outermost) variable/constant binding — this handles Haskell where main
is a top-level bind node with kind 'variable'. Widest span is used so that
nested let-bindings do not shadow the outer main binding.
- File node remains the absolute last resort.
Also adds 'variable' to NODE_KIND_FILTER_SQL (JS) and EDGE_NODE_KIND_FILTER
(Rust pipeline.rs) so top-level variable bindings are included in the allNodes
set available for caller matching.
parity-compare.mjs --langs haskell,zig --hybrid: PARITY OK — 2/2 fixtures.
* chore(lint): fix unused import and formatting in cpp/cuda extractors and test
Remove unused TypeMapEntry import from cpp.ts and cuda.ts, reformat
primitive-type Set literals and test expect() calls to satisfy biome
line-length rules.
* fix: align Java interface dispatch across wasm/native/hybrid
Java was the only fixture where all three build paths (wasm, native,
hybrid) disagreed pairwise.
Bug 1 — WASM typeMap pollution:
`handleJavaLocalVarDecl` used last-wins Map.set(), so the local
`InMemoryUserRepository repo` in the static `createDefault()` method
silently overrode the constructor parameter `UserRepository repo`.
This caused WASM to bypass the interface and resolve directly to the
concrete class, producing no interface edge and the wrong receiver.
Fix: switch to first-wins `setTypeMapEntry` to match Rust extractor
semantics. First-wins preserves the interface annotation that drives
correct CHA dispatch.
Bug 2 — native vs wasm/hybrid confidence mismatch:
`runPostNativeCha` (native orchestrator path) used
`computeConfidence − CHA_DISPATCH_PENALTY = 0.7 − 0.1 = 0.6`,
while `runChaPostPass` (DB post-pass used by wasm and hybrid) hardcodes
0.8. Fix: align `runPostNativeCha` to also use 0.8.
Result: all three build paths now emit identical edges and confidences.
`parity-compare.mjs --langs java --hybrid` passes.
Updated expected-edges.json to include both the interface declaration
edge (TypeRepository.X at 0.7) and the CHA-expanded impl edge
(InMemoryUserRepository.X at 0.8), which are the correct semantics for
an interface-typed receiver.
Closes #1469
* fix(wasm): align typed-receiver CHA dispatch confidence to 0.8
The inline CHA expansion in buildCallEdges and buildChaPostPass used
computeConfidence(relPath, t.file) - CHA_DISPATCH_PENALTY for all CHA
targets, producing 0.6 for cross-directory interface dispatch (same-dir
= 0.7, minus 0.1 penalty). runChaPostPass (helpers.ts) and
runPostNativeCha (native-orchestrator.ts) both hardcode 0.8 for
interface/CHA-dispatch edges.
The deduplication in runChaPostPass uses the existing DB edge as-is
and skips reinsertion, so the 0.6 edges from the inline pass were never
upgraded to 0.8.
Fix: typed-receiver (interface) dispatch branches now use hardcoded 0.8
matching the post-pass constants. The this/super branch keeps
computeConfidence-based proximity scoring to remain aligned with
runPostNativeThisDispatch.
parity-compare.mjs --langs typescript --hybrid goes green (was 12
edge diffs). Closes #1470
docs check acknowledged
* fix(caller): use nullish coalescing for endLine to avoid treating 0 as unbounded
Replace `|| Infinity` with `?? Infinity` in findCaller so that a definition
with endLine=0 (a valid single-line node at the start of a file) is not
incorrectly treated as having unbounded span.
* docs(caller): fix Pass 2 docstring — widest not narrowest for variable/constant binding
The find_enclosing_caller doc-comment incorrectly said "narrowest" for
Pass 2, but the code (and the TS mirror in call-resolver.ts) correctly
selects the WIDEST (outermost) enclosing variable/constant binding so
that nested let-bindings inside main's do-block do not shadow main.
* fix(bench): exempt 3.12.0 No-op rebuild from perf-canary regression gate
CI runner variance on the sub-30ms native no-op rebuild metric caused a
false positive (+396%, threshold 100%) on run 27455727444. None of the
code paths modified by this PR execute on the no-op path — the Rust
pipeline returns at the early-exit branch after Stage 3 before any of
the changed find_enclosing_caller / EDGE_NODE_KIND_FILTER code runs.
Same pattern as 3.11.2:No-op rebuild.1 parent b865d1b commit 5c9683e
8 files changed
Lines changed: 206 additions & 58 deletions
File tree
- crates/codegraph-core/src/domain/graph/builder
- stages
- src
- domain/graph/builder
- stages
- extractors
- tests/benchmarks
- resolution/fixtures/java
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1115 | 1115 | | |
1116 | 1116 | | |
1117 | 1117 | | |
1118 | | - | |
| 1118 | + | |
1119 | 1119 | | |
1120 | 1120 | | |
1121 | 1121 | | |
| |||
Lines changed: 64 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| 130 | + | |
130 | 131 | | |
131 | 132 | | |
132 | 133 | | |
| |||
473 | 474 | | |
474 | 475 | | |
475 | 476 | | |
476 | | - | |
| 477 | + | |
477 | 478 | | |
478 | 479 | | |
479 | 480 | | |
| |||
654 | 655 | | |
655 | 656 | | |
656 | 657 | | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
657 | 670 | | |
658 | | - | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
659 | 681 | | |
660 | | - | |
661 | | - | |
662 | | - | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
663 | 697 | | |
664 | 698 | | |
665 | | - | |
666 | | - | |
667 | | - | |
668 | | - | |
669 | | - | |
670 | | - | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
671 | 715 | | |
672 | 716 | | |
673 | 717 | | |
674 | 718 | | |
675 | | - | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
676 | 728 | | |
677 | 729 | | |
678 | 730 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
50 | 66 | | |
51 | 67 | | |
52 | 68 | | |
| |||
59 | 75 | | |
60 | 76 | | |
61 | 77 | | |
62 | | - | |
63 | | - | |
64 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
65 | 94 | | |
66 | 95 | | |
67 | | - | |
| 96 | + | |
68 | 97 | | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
76 | 116 | | |
77 | 117 | | |
78 | 118 | | |
79 | 119 | | |
80 | 120 | | |
81 | | - | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
82 | 135 | | |
83 | 136 | | |
84 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
709 | 709 | | |
710 | 710 | | |
711 | 711 | | |
| 712 | + | |
712 | 713 | | |
713 | 714 | | |
714 | 715 | | |
| |||
727 | 728 | | |
728 | 729 | | |
729 | 730 | | |
| 731 | + | |
730 | 732 | | |
731 | 733 | | |
732 | 734 | | |
733 | 735 | | |
734 | 736 | | |
735 | 737 | | |
736 | | - | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
737 | 746 | | |
738 | 747 | | |
739 | 748 | | |
| |||
1294 | 1303 | | |
1295 | 1304 | | |
1296 | 1305 | | |
| 1306 | + | |
1297 | 1307 | | |
1298 | 1308 | | |
1299 | 1309 | | |
| |||
1311 | 1321 | | |
1312 | 1322 | | |
1313 | 1323 | | |
| 1324 | + | |
1314 | 1325 | | |
1315 | 1326 | | |
1316 | 1327 | | |
1317 | 1328 | | |
1318 | 1329 | | |
1319 | | - | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
| 1337 | + | |
1320 | 1338 | | |
1321 | 1339 | | |
1322 | 1340 | | |
| |||
1487 | 1505 | | |
1488 | 1506 | | |
1489 | 1507 | | |
1490 | | - | |
| 1508 | + | |
1491 | 1509 | | |
1492 | 1510 | | |
1493 | 1511 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
667 | 667 | | |
668 | 668 | | |
669 | 669 | | |
670 | | - | |
671 | | - | |
672 | | - | |
673 | | - | |
674 | | - | |
675 | | - | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
676 | 674 | | |
677 | 675 | | |
678 | 676 | | |
| |||
959 | 957 | | |
960 | 958 | | |
961 | 959 | | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
962 | 968 | | |
963 | 969 | | |
964 | 970 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
273 | 274 | | |
274 | 275 | | |
275 | 276 | | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
283 | 284 | | |
284 | 285 | | |
285 | 286 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
322 | 322 | | |
323 | 323 | | |
324 | 324 | | |
325 | | - | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
338 | 336 | | |
339 | 337 | | |
340 | 338 | | |
| |||
358 | 356 | | |
359 | 357 | | |
360 | 358 | | |
361 | | - | |
362 | 359 | | |
363 | 360 | | |
364 | 361 | | |
| |||
0 commit comments