70fd5fefc2e5df262bbe0d5392d9c93c23cc98c8
155
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70fd5fefc2 |
feat(expo-router): add Expo Router support for screens and navigates
Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps. |
||
|
|
41c10750e0 |
fix(erlang): give same-name different-arity functions separate arity-qualified nodes (#1610) (#1615)
Fixes #1610. Also fixes #1358 (the `<<binary>>` arity miscount in behaviour dispatch, reported separately and hit by the same code path). ## Problem Arity is part of an Erlang function's identity — `f/1` and `f/2` are unrelated top-level definitions — but the extractor merged consecutive same-name `fun_decl`s regardless of arity. Reproduced on main exactly as reported: - adjacent `f(X) -> …. f(X, Y) -> ….` → **one** node spanning both, with the first definition's signature; - interleaved `f/1, g/0, f/2` → two nodes with **identical** `qualified_name`; - `cowboy_req`'s `header(Name, Req) -> header(Name, Req, undefined).` → a **self-loop** `header → header`, with the `-spec` for `/3` swallowed by the merged span; - `-export([f/1])` marked every arity exported. ## Fix - **One node per (name, arity).** Clauses of the same name+arity still merge (that part of the old behavior was correct); a different arity starts a new node. `qualifiedName` carries the canonical spelling — `mod::f/1` — while the node **name stays bare** so search and bare-name matching are unchanged. - **`-export` and `-spec` are per-arity.** `-export([f/1])` exports exactly `f/1`; a spec sitting between two arities attaches to the arity its signature names. - **Refs carry the call-site arity** wherever it's statically known: local `f/1`, remote `mod::f/2`, `fun f/1` / `fun mod:f/1` values, `gen_server` dispatch (`handle_call/3`, `handle_cast/2`), and spawn/apply MFA lists (`spawn_link(?MODULE, work, [A, B])` → `work/2`). - **The matcher resolves only to the named arity** — same file first (a local call targets its own module) — and when no definition of that arity exists it resolves to **nothing** rather than a sibling arity: silent beats wrong. An arity-less dynamic-MFA ref resolves only when the module defines exactly one arity of that name. - **Behaviour dispatch** selects the implementer node of the site's arity, and the arity counter now skips `<<1,2,3>>` binary-literal commas per its own docstring (#1358) — `Mod:decode(<<1,2,3>>, Opts)` counts 2, not 4. - **`codegraph_explore` / `codegraph_node`** accept the written `mod:fn/3` spelling against the new arity-qualified names (the issue's measured `cowboy_stream_h:request_process/3` shape). ## Validation Minimal fixtures (all three reported shapes) now index as `gap::f/1` + `gap::f/2`, distinct `inter::f/1`/`inter::f/2`, and a real `deleg::header/2 → deleg::header/3` edge with no self-loop. Cowboy (fresh `--depth 1` clone, this build vs unmodified main build): | | main | this PR | |---|---|---| | nodes | 3,668 | 3,748 (+80 — the arity splits; no explosion) | | erlang function nodes | 2,850 | 2,930 | | behaviour dispatch edges | 38 | **44** | | `cowboy_req::header` | one node, span 420–425, /3's spec lost | `header/2` (420–421, its own spec) + `header/3` (424–425, its spec) | | delegation | self-loop | `header/2 → header/3` | `calls` edges drop 6,059 → 5,656: a sample of every removed pair shows the false-positive class the issue predicted — out-of-repo/BIF calls (`length/1`, `error/1`, `quicer:*`) that previously name-matched onto unrelated same-named in-repo functions now stay unresolved. Tests: new arity coverage in extraction + a new arity-resolution integration suite + a #1358 binary-literal behaviour test; updated existing Erlang expectations to the arity-carrying spellings. Full suite: **3,018 passed, 0 failed**. No migration: an existing Erlang index picks the new shape up on its next re-index (`codegraph sync` / re-`init`). Erlang is wasm-only (not in the native kernel), so there is no kernel-parity surface. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK |
||
|
|
278a8edc35 |
fix(resolution): resolve calls to object-literal namespace members (#1573) (#1597)
Fixes #1573. Thanks @IAliceBobI — the report had the root cause exactly right, and the fix sits one layer up from the suggested spot (resolution rather than the container-kind set), for the reason below. ## What was wrong Methods of an exported object-literal constant — `export const api = { call() {…}, get: () => {…} }` used as a module's API surface — never received a call edge from `api.call()`, same-file or through an import. The members are extracted as plain functions with **bare** qualified names (`call`, not `api::call`) sitting inside the constant's source extent, so: - the `Container::member` lookup the class-shaped kinds use (#825) bails on kind `constant`, and even with `constant` added to that set there is no `api::call` to find; - the declared-type inference for imported singleton instances (#1292) finds no type in a literal and falls back to the constant edge; - the same-file strategies only consider classes and `method` kinds, so the call resolved to nothing at all. Net effect: `callers` / impact reported zero for methods called from everywhere, with no boundary warning because nothing about `obj.method()` looks dynamic. ## What this does Adds one helper that resolves a member **by containment** — a node named `member` whose source range lies inside the value's range, in the value's own file — and uses it from both halves: - **Import path**: when the imported value is a constant/variable, the literal member is tried right after the `Container::member` lookup and before the #1292 instance inference, so the cross-file edge lands on the method instead of the constant. - **Same-file path**: a same-file constant/variable receiver (TS/JS family only) is checked before the class-name strategies. Precision rules, all tested: calls accept callable kinds only; a declaration nested inside another member's body is not a member; nothing outside the value's range can donate a match — a same-named top-level function, or a method returned by a factory the value merely holds — so those cases keep today's behavior rather than guessing. Class statics (`C.s()`) and non-literal values are untouched. Extraction and qualified names are deliberately left alone: changing how literal members are named would have to be mirrored in the native kernel byte-for-byte, and the resolver-side lookup is contained and language-gated. ## Tests - The issue's repro end-to-end: `sameFileCallers` and `crossFileCaller` are both callers of `m`; a decoy `m` in a third file gets none; the `C.s()` static control resolves exactly as before; `crossFileCaller` no longer has a `calls` edge to the constant. - Arrow-property and method members both resolve; a `function call()` nested inside `get`'s body is never taken for `api.call()`. - A value holding a factory's result (`const obj = makeObj()`) with a same-named top-level `m` in the file: no false attribution, existing behavior kept. - The two positive tests fail on `main`; the control passes both ways, as a guard should. - Full suite: 189 files, 3181 passed / 9 skipped. With the built CLI on the issue's `a.ts`/`b.ts`: `codegraph callers m` → 2 callers (`sameFileCallers`, `crossFileCaller`); `callers s` unchanged; edges `sameFileCallers -> m` (0.85) and `crossFileCaller -> m` (import, 0.9), none to `obj`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK |
||
|
|
7963672689 |
fix(rust): resolve self.field.method() on the field's declared type instead of a same-named method (#1585) (#1599)
Fixes #1585. **Stacked on #1596** (the base branch is `fix/1588-rust-impl-type-qualification`; this PR's own diff is the second commit). Merge #1596 first, then retarget/merge this one. ## What was wrong ```rust impl Outer { pub fn run(&mut self) { self.inner.run(); // inner: Inner } } ``` produced `Outer::run -> Outer::run` — recursion the source doesn't contain. The extractor collapsed every `self.<field>.<method>()` receiver to the bare method name (`run`), so the resolver only ever saw `run` and exact-matched the nearest same-named method — the calling method itself, or a method of an unrelated type. Nothing marked the edge as a guess, and no row stayed in `unresolved_refs`, so a consumer had no way to tell. The same happened when the field's type isn't a project type at all (`its: std::vec::IntoIter<_>` → `self.its.next()`, `matcher: Regex` → `self.matcher.is_match()`): the bare `next` / `is_match` attached to whatever local method shared the name. ripgrep had 279 self-edges on `main`; the issue lists three sites, all of this shape. (The issue's C++ control — "`Outer::run -> Inner::run` resolves correctly" — doesn't actually hold on `main`: `inner.h` is classified as C by the `.h` heuristic, so `Inner::run` never exists and the C++ repro self-edges too. That's #1592, fixed separately.) ## What this does Rust struct fields are not graph nodes, so the field's type can only come from the struct's declaration text. This follows the Go 2-hop precedent exactly (`matchGoFieldChainCall`, #1276), including its exclusivity rule: 1. **Extraction (TS walker + native kernel, identical, parity-tested):** a call whose receiver is `self.<field>` keeps the owner-field shape — `self.inner.run()` is emitted as `self.inner.run`. Deeper chains (`self.a.b.m()`), call receivers (`self.f().m()`), parenthesized receivers and bare `self` keep the bare name, exactly as before. 2. **Resolution (`matchRustSelfFieldCall`):** owner type = the calling method's qualified-name prefix (`Outer::run` → `Outer`); the field's declared type is read from the owner struct's **own declaration lines** (comment-stripped, line by line — same discipline as the Go helper); the method is resolved **and validated** on that type by `resolveMethodOnType` (confidence 0.85, `instance-method`). 3. **Exclusive:** when the field is declared with an external type, a generic parameter (`T`), a container that doesn't auto-deref (`Option`/`Vec`/`Mutex`/…), or can't be found, the ref **stays unresolved** — it never falls through to the bare-name strategies. That is the safe behaviour the issue asks for, and it is what #1276 already chose for Go. `rustFieldTypeName` looks through exactly the layers Rust's method-call auto-deref looks through: references (`&`, `&'a mut`) and the owning smart pointers `Box`/`Rc`/`Arc`. `Box<dyn Source>` yields the trait, whose method node the interface-impl synthesizer then fans out to every implementation. `Option<Inner>` is left alone — `self.inner.take()` is Option's method and must not become `Inner::take`. Why it stacks on #1596: the owner is taken from the method's qualified name, which for a generic/lifetime impl was the trait's name before that fix. ## Measured on ripgrep (110 `.rs` files, #1596 build vs this branch) | | #1596 | this PR | |---|---|---| | nodes | 4029 | 4029 | | `calls` self-edges | 279 | **146** (none of the `self.<field>` shape remain — 116 bare-receiver, 30 other dotted) | | `self.<field>.m()` calls resolved through a validated field type | — | **292** (`DecompressionMatcher::command -> GlobSet::matches`, `Parser::find_long -> FlagMap::find`, `Haystack::path -> DirEntry::path`, …) | | `self.<field>.m()` calls left unresolved | — | **417** — every sampled one is a std/container method: `self.commands.push`, `self.child.wait`, `self.pre.is_some`, `self.colors.clone`, `self.path_terminator.unwrap_or` | | `calls` edges total | 9150 | 8878 (the 272 removed are the former bare-name guesses for those 417) | The issue's three sites: `walk.rs:824` now resolves to `IgnoreBuilder::add_custom_ignore_filename` (was a self-edge); `walk.rs:1195` (`self.its.next`, `IntoIter`) and `globset/lib.rs:983` (`self.matcher.is_match`, `Regex`) are parked as unresolved instead of guessed. The issue's repro gives `Outer::run -> Inner::run` (`instance-method`, confidence 0.85) on both the kernel path and `CODEGRAPH_KERNEL=0`. ## Tests - `__tests__/extraction.test.ts`: only the single-hop `self.<field>.<method>()` call keeps the prefix; deeper / call / parenthesized / bare-`self` receivers and a local receiver are unchanged. - `__tests__/resolution.test.ts` (end-to-end, Cargo layout): the issue's repro → `Outer::run -> Inner::run`, no self-edge; an external field type (`std::vec::IntoIter`) with a local `next` decoy → no edge at all; `Box<Inner>` and `&'a mut Inner` resolve, `Option<Inner>` does not (even though `Inner` declares the method); a generic `T` field → no edge; genuine `self.run()` recursion keeps its self-edge; the #1588 repro's `UsesFile::go` / `UsesBuf::go` resolve to `FileSource::read` / `BufSource::read`, and a `Box<dyn Source>` field lands on `Source::read` with the synthesizer fanning out to both impls. - `__tests__/fixtures/kernel-parity/torture.rs` grows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests). - Full `npm test` on this branch: 189 files, 3187 passed, 9 skipped, 0 failed. Re-index after upgrading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK |
||
|
|
474f051d3c |
fix(resolution): load path aliases through tsconfig extends and base configs (#1534) (#1548)
`loadProjectAliases()` read only the root `tsconfig.json` / `jsconfig.json` own `compilerOptions`, so an Nx-style monorepo — every alias declared in a `tsconfig.base.json` — got `null` back and every cross-package import fell through to name-based matching. Silently: no unresolved-import warning, and the results still look precise. Two things were missing, and either one alone leaves a common Nx layout broken: Fold the `extends` chain into the effective options before building the alias map. Relative and `node_modules` package specifiers both resolve, the nearest config wins (tsc replaces `paths` rather than merging), and a config already on the current chain is not re-entered, so `a extends b extends a` terminates instead of recursing forever. `paths` are anchored at `baseUrl` when one is declared — itself relative to the config that declared it — and otherwise at the directory of the config that declared the `paths`, which is what tsc does and what keeps an inherited `src/*` from being read as root-relative. Read `tsconfig.base.json` as a last candidate. A root `tsconfig.json` is still authoritative when it exists and reaches the base through `extends`; the fallback covers the layouts where that never happens — a solution-style root config (`references`, no `extends`, no `paths`, which is what nx's own repository ships) or no root `tsconfig.json` at all. A candidate that contributes no aliases no longer shadows a later one that does. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> |
||
|
|
a74029105a |
fix(resolution): resolve ES imports targeting .xsjs/.xsjslib files (#556) (#594)
The extraction half of #556 — indexing `.xsjs` / `.xsjslib` as JavaScript — already landed on main via #654. This PR is now scoped to the remaining resolution gap: the JS import-resolution list did not include the SAP HANA extensions, so an extensionless `import { x } from './helpers'` in a `.xsjs` file resolved to nothing and the cross-file call edge was dropped. Add `.xsjs` / `.xsjslib` to the `javascript` entry in EXTENSION_RESOLUTION so those imports resolve to their target file and `codegraph_callers` / `codegraph_impact` see the edge. One resolution test covers the .xsjs -> .xsjslib import; the now-redundant extraction/detection tests were dropped (covered by #654). |
||
|
|
340d4b033e |
fix(swift): remove catastrophic backtracking in Vapor route regex (#1547)
The arg-list group `(?:[^,()]+,\s*)*` was ambiguous: the trailing `\s*` and the next iteration's `[^,()]+` could both claim the same run of spaces, so a `.METHOD(...)` call with many comma-separated args that never reaches `use:` forced an exponential search. Measured on `app.get(arg0: value0, ...)`: 40ms at 20 args, 647ms at 24, 41.7s at 30, and no result after 120s at 60. Anchoring each repetition at a comma (`(?:[^,()]+,)*\s*`) makes the split unique — `,` is outside the char class, so there is nothing to re-partition. Same input is now 0.09ms at 1000 args. Match behaviour is unchanged: all four capture groups are identical on 18 hand-written Vapor route shapes (no args, single/multi path segments, `X.parameter`, multi-line calls, Environment.get non-matches) and on 200k fuzzed inputs. Fixes #1544 |
||
|
|
81e1f4a92f |
fix: harden daemon and large-index recovery paths (#1562)
* fix: harden indexing recovery and daemon liveness * test: cover daemon and recovery review gaps * test: pin that a failure marker never blocks a later successful parse (#1557 retry-discard guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b0c4b8b93 |
fix(resolution): trait dispatch reaches union implementors (#1515)
Making unions first-class nodes leaves the third loss in #1515 open: interfaceOverrideEdges enumerates its concrete side as ['class','struct'], so a union implementor is skipped even though it now has a real node and a real `implements` edge. "Who implements this trait" then answers wrongly rather than incompletely — the struct beside it bridges and the union does not. Add 'union' to that tuple, plus a regression test that pins the Rust trait -> union-impl hop (the struct implementor is the control proving the synthesizer ran). Verified the test fails on the union assertion alone before this change. No EXTRACTION_VERSION bump: main is already at 25 against v1.5.0's 24, so existing indexes are flagged stale for the next release regardless, and over-bumping is what turns the re-index hint into noise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e2195940fb | fix(union): complete downstream container handling | ||
|
|
e922563e05 | fix(resolution): recognize union instantiation | ||
|
|
8e3cde6994 | feat(kernel): preserve union nodes across extraction | ||
|
|
38580e0b04 |
fix(python): bare class references produce references edges to classes (#1478) (#1493)
Python's class-as-value idioms (return SomeClass, x = SomeClass, registry dicts, classes passed as arguments) produced no references edges, so callers/impact on a Django/DRF serializer missed the views that consume it. Three gates dropped them: - return_statement was never dispatched by PYTHON_SPEC (kernel mirrored) - the extraction gate (definedHere) collected function/method names only - resolution accepted function/method targets only (matchFunctionRef + the function_ref import fast path) Capture return_statement for Python (single expression; tuple returns not descended), admit same-file CLASS names to the gate, and accept class targets for Python bare identifiers — scoped to Python so the TS/JS KIND FILTER contract is untouched. The docopt false-positive mechanism behind the function-only rule (lowercase locals vs same-named methods) doesn't transfer: methods stay excluded for bare ids, and the same-file/import gate + unique-or-drop rules still apply. Probed on django-rest-framework (~250 files): 559 new references→class edges, 10/10 sampled genuine (serializer_class = AuthTokenSerializer, the ModelSerializer field-mapping registry, aliases, ctor args, isinstance). EXTRACTION_VERSION 24 → 25. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
157c8e735d |
perf(resolution): generation-tagged supertype memo + method owner index — Swift compiler 185→98s, byte-identical (#1395)
The swiftc =2/nm:mc-* attribution located the wall: the getSupertypes conformance walk ran 971,200 times (565s of combined worker time, 581µs each) — every resolveMethodOnType miss re-queried implements/extends edges for every same-named type node, recursing depth-4 through Swift's protocol landscape with no memoization, and post-inference resolveMethodOnType averaged 1,912µs per call. Fix 1 — generation-tagged getSupertypes memo. Supertype edges GROW during the resolution loop (batch k persists its edges BEFORE batch k+1 fans out — the #1320 ordering), so a plain cache would freeze an early batch's emptier answer. Within a batch the edge state is fixed by that same ordering, so memo entries carry a generation that advances at every batch entry point (resolveBatchYielding / resolveListForAdmission — covering the sequential loop, pool workers, sync admission, and the conformance pass); a stale-gen entry recomputes. Behavior-identical to no memo at every point in time; walk invocation counts match the unmemoized run exactly (971,200 / 24,336 / 76,415). Fix 2 — per-(language, method-name) owner index in getMethodMatches: candidates bucket once by their qualifiedName's last two segments (exactly the span the match predicate tests), so a (type, method) query is a map lookup instead of an O(candidates) scan per methodMatchCache miss. ObjC selectors and multi-segment typeNames keep the legacy linear path. Also ships nm:mc-rmot / nm:rmot-supers =2 attribution rows. swiftc: settle 100.2→31.3s, resolveMethodOnType 1,912→202µs, wall 183.5→97.8s (was 185s at the head-to-head; cbm's same-box number is 119.1s). Gates: swiftc old-vs-new dump byte-identical (1,837,235 rows), swiftc pooled-vs-CODEGRAPH_NO_PARALLEL_RESOLVE=1 identical (the generation-semantics risk surface), dubbo old-vs-new identical (49k Java instance-method hits share both paths), Alamofire identical; suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
974e6c8b95 |
perf(resolution): incremental receiver-inference scan memo + compiled-pattern memo — kong −8% more (−23% cumulative), byte-identical (#1392)
The kong/tokio matcher-chain residue attributed (nm:mc-* sub-stage rows, shipped here too): matchMethodCall's cost is ~entirely inferLocalReceiverType — 61µs per miss on kong, 99% miss rate (39k `self:` calls hunting a local declaration Lua never writes), re-scanning the same scope lines for every ref. Two pure memos, both semantics-preserving by construction: - Compiled-pattern memo: localReceiverTypePatterns/phpPropertyTypePatterns built 2-4 fresh RegExp objects per call; patterns are a pure function of (language, receiver) and non-global, so instances are shared via a FIFO-capped map (no per-get mutation — the §7a.6 LRU-churn lesson). - Incremental scan memo: refs for the same (file, scope, receiver) arrive in ~ascending line order and the backward declaration scan is a pure function of immutable file lines — a per-context watermark scans each line once per key (query(c) = highest match in [start..c]; monotonic calls extend the watermark over (hi..c]; non-monotonic calls fall back to the plain bounded scan). componentScoped (CFML/PHP whole-file sweep) is keyed out. States drop with the context's file caches via clearNameMatcherMemos, wired into ReferenceResolver.clearCaches. kong mc-infer misses 61→20µs (2.4s→0.8s combined); fresh index 3.43 → 3.03-3.20s (−8%; 4.07 → 3.14 cumulative with #1391). tokio unchanged (tight scopes). Gates: dubbo (49k Java instance-method HITS ride this scan), kong, tokio, Fusion dumps all byte-identical; suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
abb0a916f7 |
perf(resolution): per-context basename index for Lua/Luau require resolution — kong fresh index −16% (#1391)
The full-README competitor matrix ranked lua/kong as the largest legitimate fresh-index gap (2.71×). Stage attribution (RESOLVE_PROFILE=2) pinned it: resolveLuaRequire ran getAllFiles().filter(endsWith) FOUR times per require ref — ~7.5k string suffix scans each, measured at ~0.9ms/ref, hit or miss (2.7s combined over kong's 3k requires). Replace the per-ref full-list scans with a per-context basename → file-paths index (the cobolCopybookIndexes pattern). Buckets preserve getAllFiles() iteration order, so each suffix's candidate filter yields exactly the array the full scan produced — identical matches, identical stable sort, identical winner, dump-proven. kong: 4.07-4.27 → 3.40-3.63s (−16%). Gates: kong old-vs-new dump byte-identical (157,650 rows), kong pooled-vs-sequential identical, Fusion (luau instance-path requires) old-vs-new identical; suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Also registers cobolCopybookIndexes in clearImportResolverMemos — it was never dropped on cache clears, so post-sync copybook lookups could serve a stale file list; cache-drop is the always-safe direction. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1aa4de6eaa |
perf(resolution): adaptive pool engagement — projected-settle bar replaces the fixed 150k-ref gate for mid-run boot; tokio −23% (#1390)
The 9-language competitor matrix exposed tokio as the worst fresh-index gap: 77% of its wall was resolution running SEQUENTIALLY — 56k Rust refs sit under the fixed 150k pool gate while costing 36µs each (9× Go's 4µs/ref on prometheus). A ref-count gate can't see per-ref cost. After each sequential batch the loop now projects the remaining sequential settle from the measured rate and boots the pool mid-run when it clears 400ms. The switch rides machinery that already existed: pool boot is async and fan-out engages only when ready, admission order is mode-independent, and the #1320 edges-before-fanout invariant holds at every batch boundary regardless of when the pool arrives. Up-front engagement at >=150k refs is unchanged; 2-core/low-memory hosts still decline inside tryCreate's sizing; CODEGRAPH_NO_PARALLEL_RESOLVE still disables; downgrade permanence is preserved (one engage attempt per run). Measured (n=3, interleaved, caffeinated): tokio 3.06-3.12 → 2.40-2.57s (resolution 2,443→~1,330ms); express (tiny control) unchanged with zero engagements; dubbo unchanged (ref-count path). Gates: tokio + excalidraw adaptive-vs-sequential dumps byte-identical (87,302 / 89,903 rows), dubbo dump identical to the session baseline, suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Known follow-up: sampling the rate mid-first- batch would close the remaining ~0.3s to the forced-engage ceiling. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
082ea65f3a |
perf(synthesis): provably-empty pass gates + prefilters — render/expo/rn/mybatis stop scanning repos they can't match; iface memo (#1389)
Store-arc round 2 (#1388 follow-up). The synthesis pool barrier on dubbo carried ~1.4s of passes that provably could not emit an edge for the project: reactRenderEdges fanned out over every class before checking for a render method (now: one indexed name lookup bounds candidates — not a language gate, Java Litho-style render+setState still matches); expo/rn cross-platform pairing streamed every method row without the languages their edges require (now registry-gated: expo needs swift AND kotlin file-languages, rn needs a JS-family caller for isBridge); mybatis built its full java-method index before discovering there were no mapper-XML methods (now collects the XML side first). ifaceEdges — real work — stops re-fetching a hub interface's methods once per implementer and skips supertype-less classes before any per-class lookup. dubbo warm wall 8.49-8.79 → 8.14-8.24s (n=3/arm, caffeinated); barrier 784→435ms; the full removed pass work lands on low-core envelopes where synthesis runs sequentially. Dumps byte-identical: dubbo old-vs-new, pooled-vs-sequential, kernel-vs-wasm (441,270 rows) + excalidraw JSX-live control (89,903 rows, 46 react-render edges reproduced). Suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Also ships the diagnostics that located the round (zero cost when off): CODEGRAPH_RESOLVE_PROFILE=2 attributes per-ref time to resolveOne's strategies (stage:*) and the name-matcher's sub-matchers (nm:*); CODEGRAPH_SYNTH_TIMINGS now prints the store worker's decode-vs-SQL split. Killed by measurement, recorded in the PR: import-failure negative cache (both-outcome names exist — static imports resolve via instance-method on jvm-miss), jvm-miss early return (1,939 later-strategy edges), jsxEdges language gate (Java generics text produces jsx edges), and §4d buffer→bind on Spring repos (extract() hook forces the decoded path — kernel=0 bundles measured). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
27c3c55436 |
perf(resolution): darwin-honest memory budget — vm_stat-based availability unstrangles the resolver pool on macOS (#1388)
Post-R7b store-arc round 1, found by the dubbo warm-wall decomposition (the cbm bar): resolution's loop-stage profile showed settle=3.0s — the main thread idling on TWO resolver workers on an 11-core Mac. Pool sizing logged `size=2 (budget=1068MB)`: memoryBudgetBytes() falls back to os.freemem() when uncontained, and macOS keeps RAM deliberately full of reclaimable cache, so freemem reads ~1GB on a mostly-idle 64GB machine. The memory term then capped the pool at 2 where the CPU term allowed 6 — the macOS sibling of §7a.1's os.cpus() cpuset-blindness (that round fixed the CPU term; this fixes the memory term). Fix: darwinMemoryAvailable() reads /usr/bin/vm_stat once per sizing call and reports free + inactive + speculative + purgeable pages — what Activity Monitor calls available, the same reclaimable-inclusive convention the Linux branch already uses by crediting inactive_file back. Parse failure → null → freemem fallback; Linux/cgroup and Windows paths untouched. Measured (dubbo 4,402 files, warm, caffeinated, n=3 each): pool now self-sizes to 6 (budget 5.7-6.3GB) — wall 8.62-8.83s vs 9.67-10.87s baseline, resolution phase 6.9→5.3s, loop settle 3.0→1.9s. Matches the CODEGRAPH_RESOLVE_WORKERS=6 probe exactly (probe-before-build). Dumps byte-identical pool-6 vs sequential (441,270 lines). Second consumer unblocked: the cFnPtr LRU cache cap no longer spuriously degrades to 128 on Macs (its full-cache tier is worth ~60s at kernel scale). Suite: resolver-pool-sizing gains a darwin-gated reclaimable-pages test + an off-darwin null pin; full suite 2,689 green ×2 with CODEGRAPH_KERNEL_EXPECT=1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ce0ae30e09 |
perf(store): resolution ref-index window — kernel-scale resolution 423→276s, 8c envelope ≈11min (§4d round 2) (#1369)
Store-architecture arc round 2. The batched resolution loop reads unresolved_refs ONLY through the status index + the PK keyset pager; the other five ref indexes (from_node, name, file_path, from_name, failed_tail) serve sync-time paths — yet every per-batch DELETE of resolved refs maintained all of them, the biggest single main-thread stage on the dubbo profile (deletes 1.2s of a 5.4s resolution phase) and 50-81s at kernel scale. beginBulkRefLoad/endBulkRefLoad on DatabaseConnection, threaded as refIndexLoad hooks next to the existing bulkEdgeLoad pair with the same minRefsForPool gate (small syncs never pay): drop the five for the loop, rebuild each in one scan at the end — where the table holds only the surviving FAILED refs (resolved rows are deleted by then), so the recreate is near-free. Crash inside the window heals on the next open (schema.sql re-applies CREATE INDEX IF NOT EXISTS). Measured: - dubbo: deletes 1.2 → 0.2s, marks 0.6 → 0.3s, recreate 219ms; wall ~8.5s flat — the freed main-lane time shifts into settle (the worker lane now binds the double-buffer at medium scale). - Linux kernel 8c: resolution 423.4 → 275.9s (deletes 50-81 → 3.2s, backpressure 16.8 → 7.4s — fewer index writes mean less WAL and cheaper folds), ref recreate 10.3s. Envelope ≈ 11.0min, from the 14.8min pre-arc best; <10min-on-8c now needs ~1 more minute. Gates: dubbo/gson dumps byte-identical; linux counts exact 2,049,153/6,413,518 and dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (153 files / 2588 tests). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
69ea438bac |
perf(kernel): cFnPtr native extraction sweep — step 2, pass 230→151s across the arc (§7a.10) (#1365)
Task #5 step 2. The fuse-then-link refactor (#1364) left the extraction sweep as a clean per-file boundary: raw text in → collected facts out. This ports that sweep to the native kernel: `cfnptr_scan_files` (codegraph-kernel/src/cfnptr.rs) strips and scans a batch of 16 files per NAPI call, and the TS side only reads files, ships batches, interns the returned facts, and resolves include paths. The JS sweep remains as the fallback (no binary, feature detection against older binaries, CODEGRAPH_KERNEL=0, or CODEGRAPH_KERNEL_CFNPTR=0). Parity discipline: the JS regexes are the spec, so the scanners are hand-rolled byte machines reproducing that engine — ASCII \w/\b next to UNICODE \s (NBSP/U+2000-200A/FEFF decoded from UTF-8), alternation order, lastIndex resume, and the observable backtracking dimensions (INIT/ARRAY modifier and struct/star/bracket optionals, DISPATCH's greedy segment loop); greedy shortcuts only where backtracking provably can't rescue a match. The native stripper blanks per UTF-16 code unit, so its output is string-identical to the TS stripper — pinned by a new kernel arm on the strip differential oracle (fixtures + 500 seeded random cases). Gates, all green: new differential suite (adversarial fixture project — CRLF, NBSP, continuations, decoy strings, unterminated comments, backtracking shapes — indexed native-vs-JS: identical edge streams, plus a record-level scanner check); repo differential on git/redis/vim/SameBoy (identical, 705/852/433/180 edges); probe-hash on the live linux kernel DB reproduced f6e1713d… (279,335 rows); linux init counts exact 2,049,153/6,413,518; dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (153 files / 2588 tests). Measured (8c cg1212, quiet host): cFnPtr sub A=47.9s B=1.1 C=40.9 D=24.1 E=36.8 = 150.9s vs step 1's 179s and the pre-arc 230s (−34% cumulative); the sweep itself halved (94.5→47.9s, JS strips 132.4k→68.9k). callback-synthesis phase 199.9→171.1s. E's attributed wall grew from overlap shift under parallel synthesis; the phase total is the honest number. Full record: plan §7a.10. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c6850d737b |
perf(resolution): cFnPtr fuse-then-link — one extraction sweep + filtered verbatim linking, pass −22% at kernel scale (#1364)
Task #5 step 1 (plan §7a.8/§7a.9). The C/C++ function-pointer dispatch synthesizer swept every file's text four times (typedefs, registrations, propagation, dispatch); at kernel scale the all-or-nothing source cache declines, so that was 4.4 read+strips per file — 78s of the ~230s pass. Now ONE extraction sweep reads+strips each file once and collects typedef names, struct-node field declarations (structurally parsed, fn-pointer classification deferred until the typedef sets are complete), resolved includes, an alias-shaped-macro name set, and per-file survival filters (init type tokens, array element types, inline-struct summaries, field-assign pairs, dispatch fields/array names — interned, a few MB on linux). The linking stages then replay the ORIGINAL pass bodies verbatim: struct layouts register in kind-scan order (same-name precedence is order-sensitive), and registration/propagation/dispatch run only for files their filter proves can have side effects, lazily re-stripping just those. Filters only over-approximate (full-file no-skip scans ⊇ the real passes' jump-cursor scans), and a filtered-out file is one where every match fails the pass's own gates before any side effect — parity by construction. Macro tables stay lazy: a sizing probe found 6.1M #define lines on linux (amdgpu register headers), ruling out retention. Measured (8c cg1212, quiet host, fresh kernel init): cFnPtr pass 230s → 179s (−22%); strips 283.5k → 132.4k (4.44 → 2.08/file, 78 → 46.6s); dispatch stage 95 → 18.5s; callback-synthesis phase 250 → 199.9s. Standalone probe on the live DB: 139 → 122s. Byte-parity gates, all green: probe-hash identical on the live kernel DB (279,335 edge rows both builds); git/redis/vim/SameBoy full dumps byte-identical old-vs-new (macro tables, commands.def, #ifdef include-units, inline structs, bare arrays exercised); kernel-parity 0-diffs on git/redis/fmt/protobuf, deferral unchanged; linux counts exact 2,049,153/6,413,518 and dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (152 files / 2563 tests). Step 2 (native per-file extractor) now has its boundary: the extraction sweep, raw text in → records out. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
971a5a0483 |
perf(resolution): worker connection recycling — WAL-depth writes-under-readers fix, superphase −11.4% at 8c (#1362)
The §7a.6 anomaly probed to its mechanism with five discriminating runs (§7a.7 table): main-thread B-tree writes triple under attached readers because READERS PIN WAL checkpoint progress — the deep WAL taxes every writer page operation (deletes 42.6s pool-off vs 118.8s pool-4 on identical hardware; an aggressive 64MB valve recovers the writes but overpays +129s in full-park folds; the v2 cache resurrection was falsified — long-tail name traffic is uncacheable at any capacity). Fix: workers close and reopen their read-only connections every 8 batches at the double-buffer's worker-idle boundary (ResolverPool.recycleWorkers + QueryBuilder.rebind + a cadence call). Reopens are sub-millisecond, resolver caches survive (only prepared statements re-prepare), and the existing checkpoints advance instead of parking. Failed recycle downgrades to sequential, same as a failed fan-out. Measured (8c pool-4, linux v7.2-rc2, cadence 25 → 8 iterated): resolution superphase 715.0 → 633.6s (−11.4%), envelope best 14.8min, recreate 59.7 → 45.3s. Byte-neutral everywhere: git dumps byte-identical old-vs-new, linux dump sha 6dd1185b reproduced (10,446,478 lines), counts 2,049,153/6,413,518, suite 2517 green. 2c unchanged by construction (no pool → no recycling). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d510de4766 |
perf(synthesis): cFnPtrEdges 2.07x at kernel scale — probe-profiled, edge set hash-identical (#1341)
cFnPtrEdges was 86% of kernel-scale synthesis (306s, §7a.3). Standalone probe iterations against the live kernel DB attributed and fixed: - sliceLines split the ENTIRE file per node (~1.6M full-file splits across the B/D/E sweeps) — split once per file, slice from the line array (D 46.2->20.5s, E 94.5->69.1s). - The 128-entry strip cache re-stripped every file 4.4x across the four file sweeps (71.8s). Sizing is now memory-budget-aware and ALL-OR-NOTHING with 5% slack over the file count: a partial LRU on cyclic sweeps thrashes to ~0% hit (measured twice: cap ~61k and cap == files both thrashed against 63.8k + includes). Full-cache strips: exactly 1.0/file, 15.5s. - Pass D pre-gates field names on fieldToStructs before the two regex type resolutions — a->f = b->g matches every struct-field assignment in the tree, ~99% of them data fields. - recvTypeIn/varTypeIn compiled a new RegExp per call - cached per name. - Line numbers counted incrementally per match (ascending indexes) instead of splitting the body prefix per match; cursor rewinds between the two dispatch scans. - CODEGRAPH_SYNTH_TIMINGS now prints per-sweep walls + read/strip/nodes accounting for this pass. 278.8s -> 134.8s standalone at the 2c envelope. Output identity proven at full scale: probe edge set (merge-dedup applied, canonical sort) SHA256 21c2a971... == the original code's 274,762 edges extracted from the live kernel DB. Pass suite 15/15. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7cc23668b5 |
perf(resolution): batch-loop de-quadratic — keyset reads, changes-based guard, DB-scaled valve caps + resolve profiler (#1339)
The §7a.2 per-ref profile overturned the assumption the whole arc was built on: resolveOne owns only ~93s of the kernel-scale ~433s batch loop. Loop-stage attribution (CODEGRAPH_RESOLVE_PROFILE, shipped here) named the rest: backpressure folds 111.2s, count guard 93.9s, batch reads 54.6s, deletes/inserts/marks ~84s, settle 85.7s. - Non-progress guard O(remaining)→O(1): the per-batch COUNT(*) walked every remaining pending row (O(N²/batch) per run, 93.9s). The cleanup queries now return summed SQLite , and zero-removals-from-claimed-work is the guard signal — the DIRECT evidence the count diff inferred (a mismatched-name resolver makes keyed cleanup no-op ⇒ changes=0). A real COUNT runs only on that suspicious path and arbitrates exactly as before. - Batch reads OFFSET→keyset (54.6s→O(batch)): OFFSET re-walked the accumulated failed-row prefix every read; seeking past the last-seen rowid is prefix-independent and enumeration-order identical. - WAL valve caps scale with DB size (env still wins): every fold re-writes hot pages (#1231 in bounded form — 111.2s at the flat 256MB cap); soft=clamp(dbSize/4, 256MB, 2GB) trades ~4× fewer folds for a transient WAL ≈ project size. - CODEGRAPH_RESOLVE_PROFILE: per-outcome resolveOne histogram + loop-stage attribution, main + workers, off by default. Gates: dubbo dump byte-identical; suite 2,491 passed / 4 skipped (kernel required). Kernel-scale payoff run lands in the plan doc next. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ca88d3bd15 |
fix(db,resolution): WAL file cap + cgroup cache credit + pool/parse sizing corrections from the instrumented kernel-scale runs (#1335)
Four §7a.1 instrumented-run findings, each measured: 1. File-size trigger + truncate-at-barrier: a fully-backfilled WAL still grows the FILE without bound — the writer only restarts at frame 0 when a commit finds zero reader marks, which the instrumented run showed never happens (file marched 361→721MB through two COMPLETE backfills; 22GB by phase end). backpressure() now also trips at 4× the soft cap on raw file size and TRUNCATEs at the parked barrier; the timer path truncates opportunistically after complete backfills. Dubbo peak: 251MB → 69MB at the same 16MB valve; dumps byte-identical under aggressive folding. 2. cgroup memory credit: memory.current counts reclaimable page cache — a post-parse container read 57MB of headroom on a 6GB box and silently disabled the pool. inactive_file is credited back (the docker-stats working-set convention); the same run now reads a sane 4.4GB budget. 3. Pool at 2 cores reversed: sequential resolution measured FASTER than pooled-6-on-2 at kernel scale (853s vs 1,150s), and synthesis is Amdahl-bound by cFnPtrEdges (306s of 358s) so pooling it bought nothing. cpuCap = min(ap−1, 6), no floor: ap=2 → sequential is the fast path. 4. Parse floor of 2: one parse worker at a 2-cpuset measured 34% slower (493s vs 369s) — main + store-worker don't fill the second core. Floor restores the baseline (373.5s measured). Plus the observability §7a.1 burned three 25-minute cycles for: valve armed/fire/timer-pass/heartbeat lines, checkpoint-worker error capture, pool sizing decisions (incl. the disabled path), backpressure-hook presence — all behind CODEGRAPH_SYNTH_TIMINGS / CODEGRAPH_WAL_VALVE_DEBUG. Suite: 2,490 passed / 4 skipped (kernel required). Kernel-scale record runs with this build follow in the migration plan §7a.1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b8833fec57 |
feat(resolution): memory-aware, cgroup-honest worker-pool sizing + CODEGRAPH_RESOLVE_WORKERS (#1333)
Pool sizing used os.cpus().length, which enumerates the HOST's CPUs: inside a 2-CPU cpuset it sized 6 resolver workers (the §7a.1 false-'sequential' premise) and 8 parse workers, and at true 8-core concurrency six ~1GB workers OOM-killed a 7GB container (oom_kill=5) mid-synthesis — sizing had no memory term and no override knob. resolvePoolSize (pure, matrix-tested): explicit CODEGRAPH_RESOLVE_WORKERS override (0 disables, cap 16); CPU term max(2, min(availableParallelism-1, 6)) — cpuset-honest, floored at 2 so true 2-core boxes keep pooled synthesis's ~2×; memory term floor(budget*0.7 / clamp(0.2*dbSize, 256MB, 1.5GB)) with budget = min(freemem, cgroup v2/v1 headroom). Parse pool's core input switches to availableParallelism. Dev machines are unchanged (still 6 workers); the 8c/7GB kernel-scale container now sizes 4. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6e52295ceb |
fix(resolution): WAL containment for the pooled superphase — writer backpressure at pool-idle boundaries (#1332)
At kernel scale the pooled resolution/synthesis superphase grew a 22GB WAL on a 4.6GB DB (cg1212, §7a.1): autocheckpointing is deferred for the run, and the valve's timer-driven passive checkpoints stay perpetually partial against the pool's continuous reads — no mechanism ever completed a backfill, so the WAL accreted the whole phase's write volume, blowing disk and feeding page-cache pressure into the 8-core/7GB container OOM. The valve's writer-side backpressure() hard-cap backstop existed but was wired only into the PARSE orchestrator. Thread it into the resolution batch loop at the double-buffer's one pool-idle boundary (batch settled, next not yet fanned out), after the edge-index recreate, and through the synthesis insert loops. Parked there, the backfill completes; readers re-enter at SQLite's backfilled mark and the next persist commit wraps the WAL. Dubbo validation, same build: valve@16MB peak WAL 251MB (floor = the single-transaction edge-index recreate) vs defaults 914MB; dumps byte-identical (441,270 rows); wall unchanged (11s). Suite 2,479 green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4efc6c70e2 |
fix(scale): kernel-scale hardening — OOM-safe pass skipping + watchdog-safe index recreate (#1323)
Two hazards found by running today's full stack against the Linux kernel (70,129 files) in the cg1212 repro container: 1. The parallel-synthesis fallback retried a worker-failed pass on the MAIN thread. At multi-million-node scale a worker failure is usually a memory ceiling, so the retry would OOM the process and take the whole index with it. Above 1.5M nodes a failed pass is now skipped with a clear stderr message (its synthesized edges are absent; the index completes). Below that, the main-thread retry stays — small-scale worker crashes are transient and the retry keeps coverage. 2. endBulkEdgeLoad rebuilt all four edge indexes in one synchronous span — measured 79s at kernel scale, past the #850 liveness watchdog's 60s stall window. A daemon-triggered re-index would have been SIGKILLed right after doing the work. Now async with an event-loop yield between builds, keeping each stall to a single index (~20s at kernel scale). Validation: full Linux kernel index to completion in the repro container — 2,048,674 nodes / 6,405,964 edges, EXIT 0, zero passes skipped, on a 2-CPU VM (worst case: pool disabled, sequential resolution + synthesis) in ~27min. Phase walls: parse 6.0m, resolution 19.5m (incl. synthesis 6.3m, recreate 79s), maintenance 74s. Suite green (2444). Also adds docs/design/native-extraction-kernel.md — the spike-validated design for the native extraction kernel (Rust parse+walk over dubbo's Java: 202ms rayon / 1.07s single-thread vs 4.7s for the current wasm pipeline). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
567b4ad4be |
perf(resolution): drop non-unique edge indexes during the bulk resolution window, byte-identical graphs (#1322)
The resolution persist's measured cost is B-tree maintenance on the edges table's five indexes (offline replay of a 224k-edge resolution set: 2.8s with all indexes, 1.1s with only the unique identity index, +0.3s to recreate the rest). On big runs (same >=150k-ref gate as the resolver pool) the four non-unique edge indexes are now dropped for the batch loop and recreated in one pass each before synthesis. Why this is safe: - idx_edges_identity stays: INSERT OR IGNORE's dedup conflicts on it (#1034), and its leftmost column is `source`, so the only mid-window edge reads — resolution's supertype walks (implements/extends by source) — keep an index via its prefix (verified with EXPLAIN QUERY PLAN). - The window closes BEFORE synthesis, whose passes read kind-keyed, and on every error path (finally). - A crash inside the window heals on the next DatabaseConnection open — schema.sql re-applies CREATE INDEX IF NOT EXISTS, same recovery as the FTS bulk-load pattern this mirrors. - Concurrent readers (a daemon serving the project mid-index) stay correct; target/kind-keyed reads degrade to scans only for the window's duration. dubbo (4,402 files): persists 4.0s -> 3.0s, fresh init 11.9s -> ~11.1s, graph byte-identical. excalidraw (below the gate): untouched, byte-identical. Recreation cost ~250ms, logged under CODEGRAPH_SYNTH_TIMINGS as edge-index-recreate. Suite green (2444). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf38ef65af |
perf(synthesis): fan dynamic-dispatch passes across the resolver pool, byte-identical graphs (#1321)
The ~36 independent synthesis passes (callback/event/framework wiring) ran sequentially on the indexer's main thread — 2.0s of a 4,402-file Java repo's index, and the stage where kernel-class repos die (#1212). They now live in an explicit registry (SYNTH_PASSES) and, when the resolver pool is alive (>=150k-ref repos), fan out across its read-only workers: dubbo synthesis 2,024ms -> ~900ms (-55%), total fresh init 13.5s -> 11.9s. Graphs verified byte-for-byte identical on both the pool path (dubbo) and the sequential path (excalidraw). Why this is safe: no pass's edges persist until the ordered merge, so every pass sees the same committed post-resolution DB state in either mode, and results merge in registry order regardless of completion order — the first-seen dedup is unchanged. The pool now survives through synthesis (destroy moved after it) instead of being torn down moments before the one stage that could reuse it. Robustness: a pass that fails on a worker (crash, OOM) is retried on the main thread — a synthesizer blow-up now costs one worker instead of the whole index, which is half the #1212 story on very large repos. Also: ref-row cleanup deletes now run as one transaction with a cached statement instead of one implicit commit per 500-row chunk (mechanically fewer WAL commits; matters most on HDD-class storage). A set-based rewrite of failed-ref parking was tried, measured ~zero on NVMe, and dropped — the remaining persist cost is edge-index B-tree maintenance, not statement dispatch. SYNTH_PROGRESS_STEPS now derives from the registry (passes + fixed marks); the pin test counts registry entries plus literal __mark sites. Suite green (2444). Sequential-path timing unchanged on excalidraw. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2f3c31a97 |
perf(resolution): defer checkpoints and double-buffer persist during resolution, byte-identical graphs (#1320)
Fresh init on a 4,402-file Java repo (dubbo): 18.2s -> 13.5s (-26%), with the resolution phase going 12.6s -> 7.9s (-38%). Graphs verified byte-identical on both the pool path (dubbo) and the sequential path (excalidraw). Two changes: 1. The fastInit+pool path restored WAL for the resolver workers but left wal_autocheckpoint at its default, so the persist loop inline-checkpointed hot pages all phase long (#1231's pathology inside resolution — measured at 58% of resolution wall). Checkpointing is now deferred behind the bounded valve and folded once at maintenance, mirroring the deferWal path. 2. The resolution loop is double-buffered: batch k+1 is prefetched (OFFSET past batch k's still-pending rows, under an explicit ORDER BY rowid) and fanned out across the pool while batch k's ref cleanup runs on the main thread. Batch settle-waits dropped 2572ms -> 117ms. Correctness invariant found by the byte-identical gate and now documented in the loop: batch k+1's resolution READS batch k's edges (resolveMethodOnType walks supertype chains over extends/implements edges that resolution itself inserts), so edges must persist BEFORE the next batch fans out; only the ref cleanup overlaps. Also extends the CODEGRAPH_SYNTH_TIMINGS instrumentation with phase labels (grammar-init, parse-loop, fts-rebuild, resolver-reinit, resolution, callback-synthesis) and pool timings (worker open/resolve, per-batch mode, persist), so the next profile is one env var away. Suite green (2444). Sync path timings unchanged. Pool floor re-validated: forced-on at 40k refs is still net-slower, so the 150k threshold stands. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c472cfb52e |
fix(resolution): literal-receiver builtins and nested locals stop fabricating call edges (#1317)
", ".join(sorted(x)) resolved by bare name to a project function named join — one nested inside a DIFFERENT function, so scope alone rules the edge out. Both defects from #1230, fixed independently: 1. Extraction: a member call on a LITERAL receiver (string, number, collection, regex — across grammars) emits no call ref at all. A literal's methods are the language's builtins, never project symbols; the bare-name fallback let them exact-match any same-named project function. Silent miss, never a wrong edge. 2. Resolution: matchByExactName filters out candidates nested inside a same-file FUNCTION container unless the ref originates within that container's line range. Class members (parent is a class-like node), top-level symbols, and C++ namespace prefixes (no parent node) are untouched. requests re-index: byte-identical (813 calls edges). excalidraw: -27 edges, all literal-receiver refs by construction. The issue's repro is pinned: join has exactly one caller (format_fields), report_missing has zero project callees. Fixes #1230 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
41c2029798 |
fix(go): field-chain calls resolve via validated type inference, never bare-name guessing (#1316)
target.conn.Exec("insert") with `conn *sql.DB` emitted a BARE `Exec`
ref (the receiver chain was dropped for non-identifier receivers), and
exact-match then bound it to the only local `Exec` — an unrelated
interface's method — fabricating an internal dependency (#1276).
Extraction now keeps Go 2-hop selector chains (`base.field.Method`),
and a dedicated matcher resolves them EXCLUSIVELY via two inference
hops: base's type from the enclosing scope (#1108 machinery), field's
declared type from the struct's own declaration lines (comment-
stripped, per-line — chi's "the tree router" doc comment otherwise
donates a phantom type). resolveMethodOnType validates the target.
Package-qualified field types are followed only when the package is
in-module — `handler http.Handler` must not bind a same-named local
decoy. Failure at any hop leaves the ref unresolved: chained Go
receivers never fall through to the bare-name strategies (they were
never emitted before, so no prior recall depends on that path).
chi before/after: node count stable (1,181); 8 correct field-chain
edges gained (mx.tree.FindRoute/InsertRoute/routes, validated,
including the unexported `node` type); the removed edges are the
prior bare-name guesses on external receivers.
Fixes #1276
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
2ec877b08c |
fix(resolution): calls through an imported singleton resolve to the method (#1315)
reproStore.notifyJoinGuildStatus() after `import { reproStore }`
resolved its calls edge to the exported CONSTANT (resolvedBy:'import'),
while the identical same-file call resolved to the method via
local-variable receiver inference (#1108) — so `callers <method>`
missed every cross-file use and a widely-used method could look
unused (#1292).
resolveViaImport's member-descend now handles imported VALUES alongside
the #825 static-member case: when the base resolves to a
constant/variable, the value's type is inferred from ITS OWN
declaration lines in the exporting file (the shared #1108 pattern
table: `= new T(...)` initializers and type annotations) and the member
is resolved AND VALIDATED on that type via resolveMethodOnType. A
failed inference or validation keeps the existing constant edge —
never a fabricated one. Calls only; plain member reads still reference
the value.
excalidraw control: byte-identical graph (10,653 nodes / 19,483 calls
edges before and after).
Fixes #1292
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e1f339f732 |
fix(go): require URL-shaped paths for route detection (#1308)
cache.Put("a", 1), store.Get("config", out), bus.Handle("user.created",
h) — any verb-named method with a string first arg — were indexed as
HTTP routes (38 of 82 route nodes were false positives on the
reporter's 200 KLOC Go codebase). A registration's first argument must
now start with "/" (every router style), or be a Go 1.22
"METHOD /path" mux pattern on Handle/HandleFunc — which now also
extracts the real method instead of ANY.
Validated on go-chi/chi (212 real routes retained, all path-shaped)
and golang/groupcache (0 route nodes).
Fixes #1259
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5736e24bb6 |
perf(index): faster fresh indexing + parallel reference resolution, byte-identical graphs (#1305)
* perf(index): ~34% faster fresh indexing, byte-identical graphs Profiling a fresh init on a medium TS repo (excalidraw, 657 files) showed the main thread as the critical path: per-row SQLite statement calls, repeated import-resolution walks, and per-row FTS trigger firings, with the parse workers ~75% idle behind it. This lands the semantics-preserving tranche of fixes: - Multi-row batched INSERTs (nodes/edges/unresolved refs/name segments) behind cached per-batch-size prepared statements; row order preserved, so rowid-based resolution determinism (#1015) is unchanged. - storeFileBundle: one transaction per file instead of four; nested transaction() calls now flatten (BEGIN-in-BEGIN previously threw, so no caller depended on nested rollback). - Dedicated store-writer thread for the fresh-DB bulk path (bundles applied in file order on a single writer connection; main thread does no DB work during the parse loop). Kill switch: CODEGRAPH_NO_STORE_WORKER=1. - Bulk FTS mode: drop the nodes_fts sync triggers during the bulk load, rebuild once at the end; crash inside the window self-heals on the next open. - Per-context memos for resolveImportPath/findExportedSymbol + a per-file exported-symbol index, invalidated exactly where clearCaches() already resets the resolver's own caches. - Fast-init on completely fresh DBs (journal in memory, no fsync until the index completes; interrupted init re-runs from scratch). Kill switch: CODEGRAPH_NO_FAST_INIT=1. - MaybeYield returns undefined on the not-due path so per-ref yield checks stop paying a promise + microtask hop each. - Parse pool prewarm for bulk indexing; compile-cache enabled at CLI and worker entry points. Excalidraw fresh init: 5.11s -> 3.36s median (n=5, warm cache, M-series). Graph dumps byte-identical across init, re-index, and sync paths; full suite green (2403 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(resolution): parallel reference resolution with canonical admission Fan resolution batches across a pool of read-only worker threads, each hosting a full ReferenceResolver over its own SQLite connection; results are admitted on the main thread in chunk order, so edge insertion order, row cleanup, failure parking, and deferred post-pass queues are exactly the sequence the single-threaded loop produces. Per-ref inputs match the baseline because the sequential path already resolves each batch against the state committed BEFORE that batch. Validated byte-identical on excalidraw (pool forced on) and apache/dubbo (4,048 Java files): dubbo full index 39s -> 19s (2.05x) with identical graph dumps (91,495 nodes / 223,953 edges). The pool only engages when total pending refs clear a threshold (default 150k, CODEGRAPH_PARALLEL_RESOLVE_MIN to tune, CODEGRAPH_NO_PARALLEL_RESOLVE=1 to disable): measured on a ~58k-ref repo the workers' boot CPU contends with resolution on the same cores and makes indexing slower, so small repos keep the sequential path. When fast-init left the DB in memory-journal mode, WAL is restored before resolution only when the pool will run (readers + rollback-journal writers don't mix). Also: sqlite adapter readOnly open support. TreeCursor spine rewrite of the body walker was built, measured neutral on real repos and equal in a 20k-child microbench (web-tree-sitter's namedChild(i) is not quadratic in this binding), and rejected — per-node JS<->WASM marshaling is the floor, which a traversal swap cannot remove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
246aee8373 |
fix(ui): within-pass progress for the C fn-pointer linking pass (#1300)
Follow-up to #1299: the per-pass bar still parked on one number while a single long pass ran — on C-heavy repos that's the fn-pointer dispatch pass, which sweeps every C/C++ file four times (typedefs, registrations, field propagation, dispatch sites) and dominates the linking phase. The pass now reports a real fraction of its dominant work (scannedFiles / files×4, at the same per-16-files cadence as its cooperative yield), and the orchestrator surfaces instrumented passes' fractions as fractional steps, throttled to whole-percent movement so the UI message volume stays bounded. The mechanism is opt-in per pass — any synthesizer that a real repo shows parking the bar can adopt the same callback. Verified on the 1,342-file C repo from the report: the linking bar now moves through 88→89→90 where it previously sat at 88 for the whole pass; graph byte-identical (50,520 nodes / 148,232 edges). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad5300a601 |
fix(ui): show synthesis as a 'Linking dynamic dispatch' phase; mute node:sqlite warning spam (#1299)
Two first-run UX bugs surfaced by indexing a real 1,342-file C repo: 1. After 'Resolving refs' hit 100%, the ~40 dynamic-dispatch synthesis passes ran with no progress surface, so the bar sat frozen at 100% long enough to read as a hang (the C fn-pointer pass alone can hold for a while on C-heavy repos). Synthesis now reports per-pass progress through a new 'linking' IndexProgress phase, rendered as 'Linking dynamic dispatch'. The step total is pinned by a test to the synthesizer's actual __mark() count so adding a pass without bumping it fails loudly. 2. node:sqlite's ExperimentalWarning is emitted once per THREAD, so the main process plus every parse worker printed it mid-index, interleaved with the progress UI. All launch paths now pass --disable-warning=ExperimentalWarning: both bundle launchers, the Windows npm-shim invocation, and the CLI self-relaunch (NODE_RUNTIME_FLAGS, deliberately excluded from the re-exec gate so an older installed launcher never triggers a pointless re-exec, and version-gated off nodes older than the flag). Verified end-to-end on the same repo: zero warnings, live linking bar, byte-identical graph (50,520 nodes / 148,232 edges). Full suite green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e871c49a31 |
fix(resolution): clean up processed refs by row id so batch boundaries can't drop sibling call sites (#1269) (#1270)
Post-batch cleanup deleted resolved refs (and parked failed ones) by (from_node_id, reference_name, reference_kind) — no line/col. When one caller had several call sites to the same callee and a batch boundary split them, the first batch's cleanup removed every row with that key, including later-batch siblings that were never attempted — their edges were silently never created. On nlohmann/json this ate 422 real call edges (write_cbor's 38 to_char_type calls indexed as 11). Refs loaded from unresolved_refs now carry their row id through resolution, and all three persist paths (sync resolveAndPersist, the yielding retry pass, the batched drain loop) delete / mark-failed by exactly that id. The key-tuple methods remain only as the fallback for hand-built refs from the public API. Failed-parking gains the same precision: outcome can differ per call site (receiver inference reads the ref's line), so a sibling must not inherit another row's failure. Also untracks the zz-scratch local test files that slipped into #1268 and gitignores the pattern. Validation: red-green regression test (5 sites, batch size 2 — old code kept 2 edges, fix keeps 5); nlohmann/json re-index is a strict superset of the previous edge set (0 lost, 422 recovered, spot-checked against source). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6103f5e228 |
fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1268)
* fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1247) tree-sitter-cpp can't parse an operator_name in field position: the call_expression carries `function: <receiver>` plus an ERROR child wrapping the operator_name instead of a field_expression callee, so the extractor emitted a calls ref named just the receiver (`a`) and the edge never resolved — while the operator method itself indexed fine. Two-part fix, scoped to the explicit call form (infix `a + b` / `a[i]` need receiver type inference and are tracked in #1258): - extraction: recover the operator_name from the ERROR child and emit `<receiver>.operator+` (`->` receivers normalized, `this->` emits the bare name), like any other member call - resolution: matchMethodCall's dot pattern now admits an operator method part (cpp-gated; symbol chars failed the \w match), so receiver-type inference + resolveMethodOnType validate the target — a same-named operator on an unrelated class can't capture the edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): harden explicit operator-call recovery against real-world shapes (#1247) Validated on nlohmann/json (dozens of explicit operator[] / operator* / operator< call sites). Two refinements the synthetic fixtures missed: - normalize spaced call-site operator names (`it.operator * ()`, `other.operator < (*this)`) to the compact form definitions index as - drop the ref for a complex receiver (`obj()->operator+`, member chains ending in a call) instead of emitting a bare operator name: exact-name fallback GUESSED among unrelated same-named operators (linked a std::map operator[] call to an in-repo operator[]) — silent miss, never a wrong edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
70b1be6a21 |
fix(php): resolve method calls through $this-> properties on their declared type (#1220) (#1251)
Carries #1221 by @w0lan plus a hardening pass: property-receiver typing consults property-shaped declarations only (typed property / promoted ctor param / pseudoconstructor assignment / assignment-followed classic ctor and typed setter), so same-named locals and parameters can never mistype a property. Co-authored-by: Roman Wolan <roman.wolan@morizon-gratka.pl> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d0cd3a7d1 |
fix(sync): resolve cross-file refs when an edit adds or removes the satisfying symbol (#1240) (#1249)
* chore: ignore .kommandr/ directory * fix(sync): resolve cross-file refs when an edit adds or removes the satisfying symbol (#1240) Incremental sync scoped reference resolution to the changed files' own refs, and a completed pass deleted every ref it failed to resolve — so a symbol change in one file could never repair references in UNCHANGED files, in either direction, until a full re-index: - New-export case: a.ts imports/calls `greet` before b.ts defines it. The failed refs were deleted at index time; when b.ts later gained `greet`, nothing revisited a.ts — the calls/imports edges stayed missing while status reported a clean index. - Removal case: when a re-index (or file deletion) dropped a symbol, the incoming edges cascade-deleted and the callers — whose resolved refs had been consumed — never got a chance to rebind to an alternative definition or reconnect when the symbol returned. Fix, sharing one lifecycle: - Schema v8: unresolved_refs gains status ('pending'/'failed') and name_tail (last dotted segment, so `h.greet` is findable by `greet`). Both resolver persist paths now park unresolvable refs as failed instead of deleting them. All pending-work readers (batched drain, non-progress guard, #1187 orphan sweep, status pendingRefs) filter to pending, preserving their invariants and keeping status honest. - Sync retry: after scoped resolution, failed refs whose name tail matches a symbol name now present in the changed files are re-resolved through a per-ref-yielding path (watchdog-safe, #1091 class). Names matching >500 failed refs are skipped as external/builtin noise (#999 rationale). - Removal side: createEdges stamps each resolution edge with its originating reference (metadata.refName, + refKind when kind promotion rewrote it). When the #899 restore misses a target or sync deletes a file, the dropped edge is resurrected as exactly that ref — re-resolved in the same sync (rebinding to an alternative definition) or parked failed until the symbol reappears. Edges without the stamp (pre-upgrade, synthesized) still drop silently: reconstructing from the target's plain name would strip receiver context and risk a rebind a full re-index would never make. - Pure-removal syncs clear resolver caches so a long-lived daemon can't resolve resurrected refs against the pre-removal graph. Validated: issue repro now yields a graph byte-identical to a full re-index; move/remove-readd/file-deletion scenarios all rebind or heal; baseline-vs-new A/B on express and gin shows identical node/edge counts and no timing regression (DB grows ~25% from the parked ref rows — pure cache, reset by any full re-index). 8 regression tests added. Fixes #1240 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e76a355df5 |
fix(resolution): gate closure-collection synthesis to Swift/Kotlin and de-quadratic its line accounting (#1235) (#1237)
closureCollectionEdges scanned every method/function node in every
language, but its dispatcher patterns ({ $0( / { it( ) are Swift/Kotlin
trailing-closure syntax — on PHP/JS repos the pass can never emit an
edge, yet .push(/.add( fired its append gate on nearly every function.
Per match it computed the line via src.slice(0, idx).split('\n'), which
is O(source) per match and goes quadratic on match-dense generated
functions (two-byte content roughly doubles it). On a 12,860-file
PHP/JS app that was 20+ minutes of the "Resolving refs" tail — frozen
at 97% — and a #850 watchdog kill; profiled on CRMEB it was 127s of a
166s index for zero edges.
- Skip nodes whose language isn't swift/kotlin before any file I/O.
- makeLineAt(): lazy newline index + binary search, shared with the
emitter passes' per-file lineOf.
- Yield every 256 regex matches inside the scan's match loops so a
single pathological function can't starve the watchdog.
CRMEB (ThinkPHP, 2,913 files): 166.8s -> 72.7s, graph byte-identical.
Alamofire: closure-collection edges byte-identical (9 edges, 4 fields).
Drupal core control: graph byte-identical.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a3f90089e8 |
fix(indexing): bounded-memory yielding pipeline tail + daemon session fixes (#1212) (#1226)
Large-codebase indexing died at the end of "Resolving refs" two ways: watchdog kills of healthy work (24k-file Java on Windows, #1212 — third iteration of the #1091/#1122 class) and hard OOMs (Linux kernel scale, where v1.3.0 could not complete at any watchdog setting). Root causes: ~31 of 37 dynamic-edge synthesis passes ran start-to-finish with no yield points, several materialized whole-graph snapshots (kotlin expect/actual opened with getAllNodes() — 2M nodes in one array; the C fn-pointer pass retained every C file's contents twice plus every function node), and the post-index WAL checkpoint ran minutes of synchronous IO on the main thread, killing even a successful index at the finish line. The pipeline tail now follows the same discipline as the rest: never hold O(graph) in the heap, yield everywhere. - All synthesis passes stream node-kind scans (cursors, not arrays) and yield on time-budgeted checkpoints; language gates skip passes whose filters a project's file languages provably can't satisfy. - kotlin expect/actual filters SQL-side; c-fnptr caches are LRU-bounded, units stream one file at a time, and the all-functions array + write-only id map are gone; spring reads each .java once, not twice. - runMaintenance moved to a worker thread (own SQLite connection); per-file store commits chunk with yields behind a serialized flush chain (preserving #1015 file-order determinism); resolver warm-up streams the DISTINCT name set; resolution batch-tail and merged-edge inserts run in bounded sub-transactions. - Daemon: fixed a socket-handoff race that could leave a fresh MCP session permanently silent (client-hello tail unshifted into a flowing stream with zero listeners — the long-standing #662 test flake was this real bug); first tool call no longer queues behind the query pool's cold start (pool.ready gate). Validation: Linux kernel (70,129 files, 2.05M nodes, 6.4M edges) fully indexes in 27m8s on a 2-core/6GB container at default heap + default watchdog; llvm-project (180k files) completes under 1GB RSS including kill-and-sync recovery; synthesized-edge and full-graph parity are byte-identical vs baseline on elasticsearch/redis/vim; the ex-flaky daemon test passed 25/25 under load. Env-gated diagnostics kept: CODEGRAPH_SYNTH_TIMINGS pass/phase timings, CODEGRAPH_MCP_DEBUG hop tracing. Design record: docs/design/main-thread-stall-followup.md. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
625b4fe921 |
fix(resolution): stop per-call config-key scan that made large Java/Kotlin (Spring) indexes take ~1h (#1180) (#1210)
On a large Java/Kotlin Spring monorepo, reference resolution — not extraction —
dominated a full index (Spring Boot's ~9,650-file tree: extraction 62s,
resolution ~26min). The Spring framework resolver ran an uncached
getNodesByKind('constant') full scan + canonicalConfigKey() filter for EVERY
dotted `calls` ref (every list.add(), builder.build(), receiver.method()),
because the config-key branch gated only on "dotted java/kotlin", not on ref
kind. With ~1,100 constant nodes × ~200k dotted calls that is ~200M wasted
row-fetches/allocations.
Fixes, one theme — config-key constants bind config `references`, never `calls`:
- frameworks/java.ts: gate the Spring config-key branch on
referenceKind === 'references' (what @Value/@ConfigurationProperties emit) so
the `calls` flood skips the scan.
- name-matcher.ts: a `calls` ref no longer resolves to a yaml/properties config
node via matchByQualifiedName (service.process() vs the yaml key
service.process) — a wrong edge that also hid the real callee; it now falls
through to method resolution.
- resolution/index.ts: cache getNodesByKind in the resolver context (same
lifetime as nameCache). Fixes the same uncached-per-ref scan in the Drupal
hook_ resolver and is defense-in-depth for the Spring :prefix branch.
Measured (Spring Boot): resolution 269s→16.5s on a 4.3k-file module (16×) and
~26min→44.7s on the full 9.6k-file tree (~35×); graph byte-identical, full suite
passes. Adds a regression test (same key, two ref kinds, opposite outcomes).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4c15f84aa4 |
fix(resolution): sweep orphaned unresolved refs so an interrupted index heals on sync (#1187) (#1191)
An indexing run killed mid-"Resolving refs" (crash, Ctrl-C, the #1122 watchdog kill) left the refs it never reached parked in unresolved_refs. The git-scoped sync fast path only re-resolves changed files' refs, so those files' call edges were missing permanently — a too-small blast radius clustering by package/module (the #1187 field report: 3 of 10 caller files for a Spring @Resource-injected method) — until a full re-index. - sync() now sweeps leftover unresolved refs with the batched resolver after its scoped pass, including on no-change syncs, so a bare `codegraph sync` recovers a wedged index (and heals pre-fix indexes on the first post-upgrade sync) - the scoped pass deletes unresolvable rows too (parity with the batched path), making "rows at rest" a sound orphan signal - drop the batched loop's early break that abandoned all later batches when one batch was all-unresolvable (its rows WERE consumed — that early stop could orphan the rest of the table at init) - surface the state: `codegraph status` warns, `status --json` gains index.pendingRefs, and MCP codegraph_status tells agents the blast radius is incomplete until the next sync Verified end-to-end on a 2,414-file synthetic Spring repo: SIGKILL mid-resolution reproduces the reporter's exact 3-of-10-callers state; a bare sync now heals it to 10/10 with the edge count converging to the clean-init total; a healthy-index sync stays a no-op. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f325134e0 |
feat(extraction): add Nix language support with module-system option wiring (#324, #332 via #648 — carries #1084) (#1190)
Carries @TyceHerrman's #1084 as the functional base. Extraction + file wiring (imports/modules lists, callPackage), module-system option-path synthesizer, lexical-scope resolution gates, ABI-15 wasm rebuilt from upstream source. Validated on agenix, nix-darwin, home-manager, and nixpkgs (44,368 files, 3m49s, 1.30M nodes). Co-authored-by: Tyce Herrman <Tyce.Herrman@pm.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
99152212a9 |
feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)
Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language: full TypeScript-grade extraction via the harmony-contrib tree-sitter grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0 npm tarball), plus the ArkUI constructs that make HarmonyOS apps traceable: - @Component/@ComponentV2 structs with decorators from both grammar positions; members extract as class members with qualified names. - build() component trees: child instantiation edges via arkui_component_expression, no synthesizer needed. - Attribute chains emitted dot-prefixed and resolved ONLY against @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) — bare-name fallthrough produced 36,840 wrong edges (17% of calls) on the OpenHarmony samples monorepo. All four grammar chain shapes handled, including the detached-chain forms. - .onClick(this.handler) method-reference bindings. - ohpm workspace modules: bare imports follow oh-package.json5 file: deps (ambiguous names dropped), honoring each module's main entry — which also lets .ts consumers resolve .ets modules. - ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with wiring-site metadata: assignment-gated state->build() re-render (V1 @State family + V2 @Local/@Provider/@Consumer), @ohos.events.emitter emit->subscriber pairing on static event keys (numeric ids same-file, named constants same-module, fan-out capped), and router.pushUrl literal urls -> the target page's @Entry struct. - $r/$rawfile resource intrinsics treated as built-ins; arkts joins the web language family, value-reference edges, re-export chase, and the other TS-applicable gates. Also ships a language-agnostic index-completeness guard: indexAll stamps index_state (indexing -> complete/partial/failed), reconciles discovered vs accounted files (a loaded run silently dropped 37 files), and codegraph status surfaces truncated/partial indexes in human and --json output. Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693 files, 202,890 nodes stable across re-index, attribute false-positive audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and #988 with credit — both informed this implementation. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f8cdbe3c67 |
feat(terraform): remote-state bridge, provider aliases, moved/import/check refs (#1174)
Follow-ups noted in #1173: - cloudposse/atmos remote-state: module.M.outputs.X emits a scoped module.M:remote-output.X candidate; the resolver bridges it to the target COMPONENT's own output when every gate holds — the module source is the stack-config remote-state module, the component name is static (a literal, or component = var.X whose variable declares a literal default in the same directory), and exactly one directory in the repo matches the component name and declares that output. Dynamic (each.value) or ambiguous wiring stays unlinked. On cloudposse/terraform-aws-components: 254 remote-state bridge edges, every one re-derived from a matching source declaration (789/789 cross-directory output edges explained: 528 local-module + 254 remote-state + 7 checker-artifact false alarms under deprecated/); coverage 66.4% -> 69.1%. - provider aliases: provider "aws" { alias = "east" } is addressed as provider.aws.east so aliased and default configurations stop colliding; provider = aws.east on a resource/data block (and the values of a module's providers map) reference the selected configuration, resolved same-directory first then up the module tree — the one construct Terraform genuinely inherits from parents. The selection is no longer misread as a resource reference (aws.east). - moved/import/removed blocks reference the resource addresses they name (anchored to the file node — no phantom symbols), so a refactor's paper trail joins the graph; check-assert conditions contribute their references while check-scoped data blocks keep indexing as before. Scoped module candidates are suppressed there: module.a.aws_x.b names a resource inside a module instance, not an output. +91 edges on cloud-foundation-fabric's moved-heavy stages. Also fixes a latent test bug from #1173: cg.getNodeById is not public API (cg.getNode is) — it only passed because the asserted edge list was empty. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |