Commit Graph
170 Commits
Author SHA1 Message Date
danusha2345andClaude Fable 5.1 2521b49a9a fix(resolution): fuzzy reachability rejects a unique guess, never manufactures one
A function nested inside another function is only callable from inside its
container (#1230). matchByExactName already declined such candidates; the
fuzzy fallback did not, so a builtin method call (`res.text()`) whose only
same-named project symbol was some file's closure resolved onto that
closure at 0.5 (#1708).

and on vitejs/vite@8492422 that traded 12 correct removals for 59 wrong
additions: the repo has a dozen `resolve` definitions, most nested, so the
filter left exactly one reachable `resolve` method and the strategy
committed every `import { resolve } from 'node:path'` call in the
playground configs to it. Filtering a crowd down to one survivor is not
evidence the survivor was ever the target.

So the check sits on the ONE candidate matchFuzzy would commit to: a
unique candidate the call cannot reach is declined; a crowd stays a crowd.
Same tree, measured against this branch's own base b9ca4b7: 12 edges lost
(all fuzzy, all onto nested functions — the same 12 #1709 removes), 0
gained, fuzzy 13 -> 1, every other resolvedBy row at zero.

The two-file fixture is #1709's, credited in the previous commit; four
direct tests pin the shape: a lone unreachable closure declines, the same
closure resolves from inside its container, closure + method is ambiguous
and declines (the candidate-set filter fails exactly this one), a lone
reachable method resolves as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:12:07 +03:00
cd4e65b59c fix(resolution): a receiver-less JS/TS call never binds to a method (#1759)
`serialize(this.raw)` inside `Record.serialize`, with a module-scope
`function serialize` in the same file, resolved onto the method itself:
both were exact-name candidates, both same-file, and findBestMatch's
line-proximity term always prefers the enclosing method (#1714). In JS/TS a
call written without a receiver cannot reach a method at all — methods
need `this.`, an object, or a bound reference.

The extractor emits `this.m()` and `super.m()` under the bare method name,
so the receiver is read back from the call site's own line: when the text
there begins with the name itself and nothing but whitespace, an operator
or an opener precedes it, the call is bare, and `method` nodes leave the
candidate set before ranking. matchFuzzy declines a lone `method` survivor
for the same ref. A name the file binds itself also has no cross-file
candidate for a bare call. `this.serialize()` and `other.serialize()` are
unchanged.

Rebased #1735 onto current main (resolved conflicts with sealed-module /
cross-file visibility guards from #1719/#1730/#1731).

Fixes #1714

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 02:22:16 -05:00
Max HsuandGitHub a6f52d737a fix(resolution): keep the existence probe inside the project root (#1631)
Fixes #1631.

Rebased contributor PR #1632 onto main (post-#1749). Lexical containment for `fileExists` filesystem fallback via `lexicalPathWithinRoot`; #935 in-root symlink behaviour preserved.
2026-09-08 00:32:05 -05:00
edcd36e5f0 fix(resolution): resolve module-qualified calls colliding with builtin methods (#1749)
isBuiltInOrExternal treated ledger.append as list.append unless the receiver
matched a known class, so real module exports never reached resolveViaImport.
Allow project-module receivers (verified via resolveImportPath) through while
keeping stdlib/PyPI silent. Completes #1681 after #1748 fixed the FP half.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:27:27 -05:00
bb1d3093eb fix(extraction): never fabricate an edge from a call-result receiver (#1748)
A member call whose receiver is itself a call — `d.setdefault(k, []).append(v)`,
`make().run()` — used to drop the receiver at extraction time, degrade to the
bare method name, and exact-match any top-level project symbol of that name
(Python and JavaScript/TypeScript). Keep the inner callee encoded as
`<inner>().<method>` in the TS extractor and native kernel; the name-matcher
refuses to guess for that shape (store-accessor exception only). Based on
#1692, rebased onto main after #1746. Fixes #1683.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:23:08 -05:00
bffd50e4f1 fix(resolution): a binding in a module that exports nothing is not a cross-file candidate (#1719) (#1746)
* fix(resolution): a binding in a module that exports nothing is not a cross-file candidate

On vitejs/vite, 157 cross-file `imports` refs — every `import { defineConfig }
from 'vite'` in the playground and the create-vite templates — resolved onto
`playground/ssr-html/test-stacktrace.js::vite`, which is `const vite = await
createServer(...)` at module scope in a file with zero exports.

Neither existing guard can see it. `isLexicallyReachable` returns early for any
candidate that is not a `function`, and the bare-import guard correctly declines
because `vite` IS a workspace member, so the specifier really is project-local.
What is wrong is only which node the name lands on.

A JS/TS file that contains an `import` statement and no export of any form
offers nothing to any other file, so none of its bindings is a candidate for a
cross-file name match. Applied in both name-based strategies: declining in
matchByExactName alone just hands the same target to matchFuzzy, which resolves
a unique candidate on its own.

Narrow on three axes, each a class this would otherwise get wrong in the
opposite direction: a classic script is exempt (a top-level binding really is a
reachable global), CommonJS is exempt (`module.exports` and `exports.x` count as
exports), and every non-JS/TS language is exempt. The export test reads source
rather than the node's `isExported` flag, because that flag is set only from an
`export_statement` ancestor and so reads false for `const x = ...; export { x }`.

* fix(resolution): count bracket CommonJS exports and `declare global` as exports

A file writing `exports["x"] = …` exports x, and a file with a `declare
global` block contributes every name in it to every other file whether or not
it exports anything of its own — the extractor emits nodes for the ambient
`var` and `interface` members, so sealing such a file would hide names that
really are reachable everywhere. Neither shape occurs on the vite corpus, so
this changes no measured count; both are now covered by the test.

* test(resolution): bind the #1719 fixture without a bare import

The consumer bound every name from 'some-external-pkg'. A bare specifier
names a package that is not in the graph, so no project node is the right
target for such a reference and #1715 declines it -- which made four of the
five positive assertions depend on a resolution that should not happen, and
they failed the moment this branch was stacked on #1715. Free references
reach the same exact-match path without asserting that.

`strayVar` was not testable at all: a bare identifier read emits no edge, so
that assertion only ever passed through the bare-import binding. The
`declare global` coverage moves to an interface reached through a type
annotation, paired with an identical file whose interface is not in a
`declare global` -- so the assertion turns on that clause rather than
passing whichever way the guard goes.

* docs(changelog): record the sealed-module guard under Unreleased

* fix(resolution): the sealed test rejects fuzzy's survivor, never filters its set

matchFuzzy declines an ambiguous name outright, so filtering sealed
candidates out of its set can leave a lone survivor and manufacture a 0.5
edge from an ambiguity that would have been declined. Testing the single
survivor instead closes that path; matchByExactName keeps the filter,
because it ranks a crowd rather than declining one.

No instance on vitejs/vite either way (row-identical, LOST 0 / GAINED 0
per #1720 review). It also declines one shape the filter form resolved: a
sealed same-language survivor no longer yields to a cross-language
candidate at 0.3.

* fix(resolution): reject invalid fallback targets without retargeting

---------

Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:13:49 -05:00
2c251e2c61 fix(resolution): a definition its language makes file-local is not a cross-file target (#1731) (#1745)
* fix(resolution): a definition its language makes file-local is not a cross-file target

Name matching accepted any same-named definition as the target of a call
from another file, however the language scopes it. isVisibleAcrossFiles
now declines, for a candidate in another file:

  C / C++   a function whose definition line carries `static` (read from
            source — the extractor records no storage class, and the kernel
            arm would need the same field)
  Kotlin, Java, C#, Swift, Scala, Dart, PHP
            visibility === 'private'
  Go        a lowercase identifier from another directory (by the name's
            case: the extractor's isExported is unset for every Go method)
  Rust      a non-`pub` item unless the reference is in the item's module
            subtree (a child sees its ancestors' private items via super::);
            a method in an `impl Trait for Type` block has the trait's
            visibility and is exempt

The test runs in ReferenceResolver on the target the whole name-matching
pipeline settled on, so a rejection ends the reference unresolved. Declining
inside matchByExactName instead let the ref fall through to matchFuzzy,
which committed to a same-language namesake the ranking had passed over —
eight edges on one tree, all onto a local `const fail = …` arrow the graph
does not hold. matchFuzzy checks its own survivor too; nothing runs after it.

Five corpora, all against b9ca4b7, wasm arm, edge rows keyed with
resolvedBy:

  betaflight fork (2,109 C files)        LOST 4,451  GAINED 0   (#1730)
  Android/Go/JS app (114 kt, 42 go)      LOST   142  GAINED 0   (#1731)
  emmc-reader-gui (71 rs)                LOST   195  GAINED 0
  skylab_hub (35 rs)                     LOST    92  GAINED 0
  vitejs/vite (JS/TS only)               LOST     0  GAINED 0

Samples read back: `Vec::new()` onto a private `fn new` in another crate,
`ui.add(…)` (egui) onto a private `add`, `latch.await()` onto a test file's
`private fun await`, `leaflet.js` onto an unexported Go `func add`,
`usbd_get_descriptor` onto a `static get_device_descriptor` in a USB class
file it never links.

Fixes #1730. Fixes #1731.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(resolution): a static in a header is part of every unit that includes it

The C rule declined any `static` function defined in another file. A
`static` in a SOURCE file is local to that translation unit and the rule is
right there; a `static` in a header — `static inline`, the whole of
MAVLink's generated `mavlink_msg_*.h` — is textually included into every
unit that names it, and the call is real. On the betaflight tree 4,306 of
the 4,451 rows the first cut removed were exactly that: `testsuite.h` and
`mavlink_msg_*.h` calling `protocol.h`'s `_mav_put_char_array`,
`mav_array_assign_char` and each other's `_pack` / `_decode` helpers.

The rule now applies only to a candidate whose file is a translation unit
(`.c .cc .cpp .cxx .c++ .m .mm`). Same tree: LOST 145, GAINED 0, every one
onto a `static` in another `.c` — STM32 USB class sources onto GD32's
`usbd_enum.c`, and the USB descriptor table shape from #1730. Header
targets in the removed set: 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): cite #1731 and narrow the C file-local note

Rebased #1732 onto latest main. Clarify that only a static in another
source file is declined (header static inline stays), name the Kotlin/Go/Rust
shapes from #1731, and note the post-pipeline placement that avoids fuzzy
fallback.

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:00:39 -05:00
Colby McHenry 882ea143e8 feat(steps): lay out screen pictures by region and render region captions
Adds region-based layout support for screens: steps now carry region information, and the server packs regions into dedicated bands with per-region captions. UI changes introduce RegionCaption and region-aware step rendering; StepsModel and related views (StepsView) consume region data, while the region-aware layout keeps anchor and region boundaries intact. Tests and docs updated to reflect region-driven organization and visualization of screen regions. This enables visualizing a screen’s picture as region-based columns rather than a single distance-driven row.
2026-08-31 15:38:13 -05:00
Colby McHenry 6f4887db80 feat(steps): draw all arms of conditional navigations as separate edges
Adds multi-arm navigation support: when a destination is produced by a conditional, every arm is now drawn as its own edge. Introduces helpers (hrefArms, destinationsForHref) and updates framework resolvers and edge creation to emit multiple navigates edges (via alsoTargets) instead of a single one. Also introduces per-app rooted route tables to avoid cross-app crossings, and updates various resolvers (React Router, TanStack Router, Vue Router, SvelteKit, Vue, and SvelteKit’s linker) and the UI to reflect multiple possible destinations. Tests and docs updated to reflect the new behavior, ensuring the Screens tab shows all possible navigation paths from conditional destinations. This makes navigation visualization more accurate for forked destinations.
2026-08-31 11:34:26 -05:00
Colby McHenryandClaude Fable 5 02430ccc32 feat(steps): a reply that sets no status is a 200; inline Express handlers keep their replies
- effects.ts implicitResponseStatus: a body-sending reply with no status in its chain (res.json / send / render, reply.send, c.json, NextResponse.json, JSONResponse / jsonify / render_template, Rails render, Laravel response()->json) is a 200; a variable status, end, sendStatus and redirects stay as they were
- branch-guards callSiteInTree: a status set by the statement just before the reply (`res.status(202); res.json(user)`) is that reply's — looked back within the block, only a statement that IS the status call counts
- steps.ts: explicit chain/args → set-before → implicit 200
- express.ts: an inline handler's reply calls (`res.status(404).json(…)`, `res.json(user)`) are references at their own line and column instead of framework noise, so the route's own reply box exists
- tests: servers fixture (inline route's 200 beside the service's 404; a 202 set before), ui-effects
- CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 12:00:04 -05:00
Colby McHenryandClaude Fable 5 bc45e9071d feat(routes): FastAPI prefixes, ASP.NET endpoint groups, wrapped server actions on the Screens tab
- python.ts postExtract: APIRouter(prefix=) and literal include_router(prefix=) composed down the include tree (module import, alias, local); a computed prefix leaves that mount alone; full-stack-fastapi-template 23 routes named by path
- csharp.ts: handler-first MapPost(Handler[, "path"]) under the endpoint-group class, the app's $"/api/{groupName}" head read in postExtract, RoutePrefix honoured; detection covers Endpoints/ files; CleanArchitecture 10 routes
- tier-synthesizer: a type argument between a client call and its parentheses (useSWR<T>(…), ky.get<T>(…)); recorded callee without it
- screens.ts: a file-scope navigation attributed to the value spanning it; a value nothing calls attributed to the functions mentioning it in importing files (request-time source read, bounded); steps.ts lends navigates edges to a value root
- tests: servers fixture (FastAPI prefixed routers, ASP.NET endpoint group end to end), frameworks.test.ts (group form, RoutePrefix), cross-tier (generic useSWR)
- docs: CHANGELOG, plan, playbook rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:52:40 -05:00
Colby McHenryandClaude Fable 5 9c6bc23b21 feat(screens): Next.js as a Screens app — pages, route handlers, links and redirects
- frameworks/nextjs.ts (split out of react.ts): App Router app/**/page.tsx and Pages Router pages → routes named by path ((group) stripped, [slug] → :slug, [...all] → :all*), bound to the default export; app/**/route.ts exports → METHOD /api/… endpoints referencing their functions; pages/api → ANY; resolve() claims router.push/replace/prefetch, redirect/permanentRedirect and NextResponse.redirect(new URL(…)) into navigates edges via the Expo href readers, against a Next-only route table gated on the app's root
- next-router-synthesizer.ts: <Link href> and internal <a href> → dashed navigates edges from the component (next-link, registeredAt)
- expo-router.ts: href readers exported; matcher accepts :param / :all* segments
- steps.ts: a Next page's own work fires from page load; a Next page makes the project a web app; {status: 201} read off the call site (branch-guards CallSiteText.status) for response rows
- frameworks/package-deps.ts: nested package.json files probed on disk (getAllFiles lists only sources); Express/React/Expo/Nest detectors use it; routing manifest names constant handlers
- tests: nextjs.test.ts (file→route rules, extract, verbs, end to end with Screens and Steps); frameworks.test.ts Next cases moved to the Next resolver
- docs: CHANGELOG, spec §3.12 frameworks paragraph, CLAUDE.md, synthesis doc, plan P4 built, playbook rows for Next / MERN / Nest channels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:40:14 -05:00
Colby McHenryandClaude Fable 5 b1f40c57dd feat(steps): cross-tier channels — a client's fetch onto its own route, queue jobs onto consumers, bus and socket events onto handlers
- resolution/tier-synthesizer.ts: http-client (literal fetch/axios/ky/got/$fetch paths, axios.create baseURL instances, template holes as :params, base-URL holes by a two-segment tail; unique match only), queue-job (BullMQ/Bull add ↔ @Process/@Processor, WorkerHost process, new Worker, queue.process), event-bus (EventEmitter2 emit ↔ @OnEvent with globs; socket emit ↔ @SubscribeMessage / socket.on both ways with tier); channel, tier, callee, registeredAt on every edge; generic transport events never pair; test and generated files never sources; registered before the emitter pass
- steps.ts: crossing() reads tier/channel before languages; an endpoint reached across a tier is a bridge box and a boundary like a screen (through=1 enters it); a channel's call is not also an effect; sites read as written; a Next 'use server' action is a crossing by its directive (when.ts directive); a function-valued constant handler (asyncHandler(...)) is a route root and borrows the file-scope calls and refs within its lines
- express.ts: app.use('/prefix', router) mounts composed onto route names in postExtract (nested, by import or require); chained router.route('/x').get(h).put(h2) extracted, across lines
- frameworks/package-deps.ts: dependencies read from workspace package.json files too (Express, React, Expo Router, NestJS detect)
- routing manifest names constant handlers; e2e/ is a test directory; explore's Flow section labels the new channels
- tests: ui-steps-cross-tier (monorepo fixture: Next client + Express/Nest API), servers test updated for the queue landing
- docs: CHANGELOG, spec §3.13 cross-tier paragraph, CLAUDE.md, callback-edge-synthesis.md, plan P3 built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:31:01 -05:00
Colby McHenryandClaude Fable 5 950686def4 feat(ui): Steps for servers — route roots, server effects, request/decorator triggers, guards for Python/Java/Kotlin/C#/Go/C
- api/route-roots.ts: the symbol a route runs (references-edge handler, exported page component, or the route itself for an inline handler), shared by steps and screens; the bare Steps tab lists an API's endpoints by router file
- api/effects.ts: database / response / queue / email / payments / cache / auth / process / network / storage / device / telemetry, matched on the call as written per language family, with model + read/write and the literal status on a response site
- graph/branch-guards.ts: callSitesForFile (the whole member chain), memberTypesInTree, decoratorsForFile, request/decorator triggers with the middleware/guard chain; guard + argument rules for Python, Java, Kotlin, C#, Go and C
- steps.ts: classify on the chain before trusting a name match, retarget this.x.y() by declared type, skip test doubles after the effect pre-check, project kind on the wire
- viewer: kindWord/kindWords per project kind, endpoint chooser, response boxes labelled by status codes
- python.ts: FastAPI detected from a monorepo sub-directory; is-test-file: samples/examples package paths are not tests
- tests: ui-steps-api-servers, ui-effects, branch-guards-languages; spec §3.13 Servers paragraph, CHANGELOG, plan doc

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 13:45:38 -05:00
Colby McHenry 873f133c96 feat(expo-router): add Expo Router support for Screens and navigations and introduce Steps API
Introduce Expo Router integration with a new Screens view and API to surface screens and transitions, plus a new Steps API and UI to depict typed steps from anchors or symbols. Extend codegraph’s extraction and resolution to handle namespace objects (export default NAME, two-statement forms, and default bindings) and React hook bindings for handlers, improving accuracy of flows across JS ↔ native boundaries. Add Swift/React Native bridge receiver evidence (RCT_EXTERN_MODULE, RCT_EXTERN_METHOD) and related resolution logic, with tests covering namespace-object resolution, useCallback-driven handlers, and inline RN event listeners. Update UI to include a Steps tab and associated components (StepsView, StepNode, ScreenEdge) and wire navigation to expose steps-based exploration via /api/steps and UI routes. Documentation and changelog reflect the new Expo Router integration and steps surface capabilities.
2026-08-28 09:46:50 -05:00
Colby McHenry 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.
2026-08-27 22:52:18 -05:00
Colby MchenryandGitHub 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
2026-08-26 10:39:15 -05:00
Colby MchenryandGitHub 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
2026-08-26 10:38:13 -05:00
Colby MchenryandGitHub 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
2026-08-26 10:37:50 -05:00
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>
2026-08-22 11:55:01 -05:00
Max HsuandGitHub 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).
2026-08-22 11:53:36 -05:00
Max HsuandGitHub 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
2026-08-22 11:53:26 -05:00
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>
2026-08-20 12:53:49 -05:00
Colby McHenryandClaude Opus 5 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>
2026-08-07 21:23:06 -05:00
ctype_lab e2195940fb fix(union): complete downstream container handling 2026-08-06 17:50:25 +09:00
ctype_lab e922563e05 fix(resolution): recognize union instantiation 2026-08-06 17:11:26 +09:00
ctype_lab 8e3cde6994 feat(kernel): preserve union nodes across extraction 2026-08-06 17:11:26 +09:00
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>
2026-08-01 01:36:07 -05:00
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>
2026-07-21 10:56:40 -05:00
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>
2026-07-21 00:20:03 -05:00
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>
2026-07-21 00:02:02 -05:00
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>
2026-07-20 23:20:28 -05:00
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>
2026-07-20 21:15:58 -05:00
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>
2026-07-20 20:11:31 -05:00
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>
2026-07-19 23:24:14 -05:00
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>
2026-07-19 17:48:27 -05:00
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>
2026-07-19 11:43:06 -05:00
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>
2026-07-18 16:58:03 -05:00
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>
2026-07-17 12:20:17 -05:00
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>
2026-07-17 11:27:30 -05:00
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>
2026-07-17 08:56:26 -05:00
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>
2026-07-17 07:54:31 -05:00
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>
2026-07-17 07:48:34 -05:00
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>
2026-07-16 19:09:01 -05:00
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>
2026-07-16 18:26:30 -05:00
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>
2026-07-16 18:07:51 -05:00
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>
2026-07-16 17:26:29 -05:00
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>
2026-07-16 15:55:38 -05:00
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>
2026-07-16 15:49:24 -05:00
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>
2026-07-16 15:36:33 -05:00