Commit Graph
519 Commits
Author SHA1 Message Date
7b339373b4 fix(extraction): warn when parse errors leave no symbols (#1522) (#1799)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 16:17:28 -05:00
4fd816b610 fix(cli): index <path> never rebuilds an ancestor instead (#1524) (#1797)
An explicit path names the project to rebuild; it is not a hint to go
looking for one. resolveProjectPath's upward walk is right for a query run
from a subdirectory, but for a full re-index it silently rebuilt the
nearest initialized parent's graph when <path> had no index of its own.
Refuse with the parent's path and the way to index <path> itself; a bare
`codegraph index` still resolves from cwd as before.

Fixes #1524

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 16:00:58 -05:00
374b3b4209 fix(resolution): constrain inheritance/import reference target kinds (#1536, #1537) (#1796)
* fix(resolution): gate extends/implements to real supertypes

An inheritance reference bound to whatever local symbol shared its name.
The name-matcher scores node kind as a bonus, never a filter, and awards
no bonus at all for inheritance refs, so `use std::error::Error;` +
`impl Error for MapperError {}` resolved to the local `MapperError::Error`
VARIANT — an implementation relationship absent from the source.

Two changes, both needed. Filtering by kind alone was measured and it
only RELOCATES the false edge: with enum members excluded, the same 7
refs moved onto an unrelated local `type Error` alias, which is a legal
supertype kind and therefore harder for a consumer to reject.

1. Eligibility before ranking. `matchByExactName` restricts its candidate
   pool to kinds that can BE a supertype, so a legitimate trait outranks
   a same-named variant instead of merely losing its edge. `resolveOne`
   is wrapped by a gate that applies the same set to every other strategy
   at one seam — filtering inside the name-matcher would have missed the
   framework, import, chain and CFML paths.
2. Locality. A name imported from outside the repository has no in-repo
   referent at all, so no candidate is correct. Only oracles that cannot
   be wrong are consulted: Rust `use` paths rooted at a stdlib crate, and
   `isExternalImport` for ES modules. Generalizing the Rust side to "the
   module path doesn't resolve to a file" was tried and reverted — a
   crate re-exporting a sibling's modules (`pub use pupil_core::ports;`)
   has no directory to walk, and that version deleted 13 real trait
   implementations.

Measured on a Rust/Tauri project (2,682 nodes): the 11 false inheritance
edges are gone, all 59 real trait relationships are preserved, and node
count is unchanged. On this repository as a control, the only edge
removed is a class recorded as extending a function. Synthesized-edge
counts are identical in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(resolution): an import never resolves to a member of a type

`import * as path from 'node:path'` is unresolvable — the module is
external — so the name-matcher fell back to finding any node called
`path`, and a common word like path/url/join/get matches a class property
or interface method somewhere in almost any repo. Nothing in any
supported language lets an import bind to a member that only exists
inside a type; you import the type.

Same shape as the inheritance gate that precedes it: eligibility applied
to the candidate pool before ranking, plus the resolveOne gate as the
backstop for every other strategy.

On this repository as a control: 19 imports pointing at methods and 4 at
properties are gone (all of them coincidences — `Walker::join`,
`Telemetry::events`), 3 refs now find the module constant they actually
name, node count unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(resolution): classify SFC script imports as ES module specifiers

`isExternalImport` had a TS/JS branch listing typescript/tsx/javascript/jsx/
arkts, so for Svelte, Vue and Astro it fell through every branch and returned
false — "not external" — for `import { Foo } from 'some-npm-pkg'`.

An SFC imports inside its `<script>` block (Astro: the `---` frontmatter) with
ordinary ES module syntax; `extractImportMappings` already routes all three
through the same `extractJSImports`. So the classifier disagreed with the
extractor about what those imports are.

Effect on the preceding commit: its locality check asks `isExternalImport`, so
it silently did nothing for SFCs. A class in a `.svelte`/`.vue`/`.astro` file
implementing a type imported from an npm package still bound to whatever local
class shared that name — verified against this branch before the fix, all three
languages.

The language set is now one constant used by both the classifier and the
locality check, so they cannot drift apart again. Relative and aliased
specifiers are unaffected: the branch returns "not external" for `./…`,
workspace members, tsconfig alias prefixes, `@/`, `~/` and `src/` exactly as it
does for `.ts`.

No edge changes on this repository as a control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: ctype_lab <cksgud1226@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:49:10 -05:00
040ba388da fix(php): resolve static calls through import aliases (#1545) (#1795)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 15:11:20 -05:00
a961491cea fix(extraction): keep git-aware scans working before Git 2.36 (#1793)
Land upstream PR #1604 by @maxmilian from commit
27c149524d968485164fb43bdec994fdb9323c75 on current main cece0720.
Fixes #1549.

Use the upstream lsFilesStaged helper at both collection and embedded-repo
discovery call sites. Retry without --recurse-submodules when staged
recursive listing fails, retaining the mode bits needed for gitlinks.

Keep the upstream regression test byte-for-byte and preserve main's
existing extraction/watcher changes, including #1728. Resolve the
changelog conflict with a concise Unreleased entry, and keep the existing
collectGitFiles documentation attached to its function.

Verified on Linux x86_64 with Node 22.19.0 and Git 2.47.3:
- The unchanged upstream test fails against main: expected [ 'a.ts' ]
  to include 'dir_b/b.ts'; it passes with the fix.
- The same persistent fixture under a PATH shim rejecting -s with
  --recurse-submodules (exit 128) changes scanDirectory from [a.ts]
  to [a.ts, dir_b/b.ts], and gitlink watcher roots from [] to [lib/].
- Real-Git controls return both files and the watcher root in both arms.
- 69 related scope/config tests and 23 extraction scanning tests pass.
- npx tsc --noEmit passes.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: newshowardz777 <newshowardz777@users.noreply.github.com>
2026-09-08 14:37:46 -05:00
cece0720e3 fix(resolution): resolve this.<field>.<method>() on the fields declared type (#1496) (#1792)
Land the six-file fix from upstream PR #1691 by danusha2345
(pr-1691 at 6d0e80d52ae953b22d615bd20c4a4c7758814e60), preserving
wasm/native extraction parity and exclusive field-type resolution.

Preserve coexistence with the #1566 Map/collection fix merged in #1790,
including nested holder.values.get coverage and the unchanged #1566
Unreleased changelog bullet. EXTRACTION_VERSION remains unchanged.

Align the existing chained-receiver regression with the fix: a declared
service field calls its method, while an anonymous field type does not
bind to unrelated same-named project functions.

Verified on Linux with Node 22.19.0:
- Rebuilt the native kernel and TypeScript/browser distribution.
- Both backends change Outbox::send -> Outbox::send into
  Outbox::send -> Mailer::send, keep Relay::forward -> Mailer::send,
  and store no self-edges in the issue repro.
- Wasm: 224 tests passed; native kernel: 255 tests passed, no skips.
- All 10 #1566 resolution cases pass on each backend, plus all four
  nested-receiver extraction parity cases.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 14:24:04 -05:00
de5adba7ea fix(resolution): leave built-in Map calls unresolved (#1566) (#1790)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 14:04:43 -05:00
72c1ff13cc fix(resolution): restore orphan-sweep parity after interruption (#1577) (#1789)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 13:45:22 -05:00
b8d46d13c7 fix(mcp): land #1624 opt-in explore dedup (#1788)
Land the #1624 approach by @danusha2345: re-serve source by default so
subagents and compacted contexts never inherit stale already-sent pointers.
Keep cross-call dedup available through explicit truthy
CODEGRAPH_EXPLORE_DEDUP values for durable contexts.

Preserve current Unreleased entries, credit the contribution, and align
the MCP server guidance with the safe default.

Validation on Linux / Node 22.23.2:
- Reproduced default-on failure before the fix; default-off now passes.
- Focused explore-cross-call-dedup suite: 26 passed.
- TypeScript: npx tsc -p tsconfig.json --noEmit passed.

Fixes #1620.

(cherry picked from commit 63992facabbbcef2797167dcdd90695d3802b533)

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 13:13:03 -05:00
7be699cd91 fix(resolution): resolve Python aliased module imports (#1626) (#1785)
* fix(resolution): resolve Python module members through an aliased from-import (#1626)

resolvePythonModuleMember rebuilt the submodule's dotted path by joining the
import source with the LOCAL name. Under 'from pkg import mod as alias' that
produces 'pkg.alias' — a module that does not exist — so the file lookup found
nothing and the call fell through to unresolved_refs with status='failed'.
codegraph_callers then reported the target as having fewer callers than it
does, which is the same wrong 'is this dead code?' answer #578 produced for
the unaliased form.

Join with the exported name instead. For an unaliased import the two names are
identical, so nothing changes there; '*' (the namespace form) keeps using the
local name, which is what it already bound to.

Scope note: the issue also reports 'import top as alias' failing. That form is
a namespace import and binds at source, so it resolves on current main — a
probe against the reverted resolver confirms it already produces its call edge.
The regression test pins both halves so the working one cannot silently break.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit f7a8940e679b5d4093dd2d00412306a6b4800723)

* fix(resolution): restore aliased Python module import edges (#1626)

Use the exported module name in the file-import resolver, matching the member resolver from upstream PR #1635. Keep both aliased call assertions and verify the file-to-file imports edge in the #1626 regression test. Update the Unreleased note to cover file dependencies.

Validation on Node 22.19.0: npm run build; supplied cg1626 repro; vitest run __tests__/resolution.test.ts -t 1626. Pass evidence saved in /workspace/cg1626-PASS.json and /workspace/cg1626-VERIFY.json.

---------

Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 12:55:13 -05:00
2f8cce5c57 fix(installer): honor CLAUDE_CONFIG_DIR and CODEX_HOME (#1627) (#1783)
* fix(installer): honor CODEX_HOME for the Codex global install (#1627)

Codex resolves its user layer from `CODEX_HOME` and only falls back to
`~/.codex`. The target hardcoded the fallback, so a user on a custom profile
got a correct install into a directory Codex never reads — the MCP entry, the
AGENTS.md block, and detect() all pointed at the wrong profile, and the
failure is silent.

Resolve the global config dir from `CODEX_HOME` when set and non-blank,
mirroring what the copilot-cli target already does for `COPILOT_HOME`. Only
the user layer moves; the project layer (#1531) stays anchored to the project.

The test harness now also clears `CODEX_HOME` in setHome() alongside
HERMES_HOME/COPILOT_HOME — without that, the existing codex tests fail on a
developer machine that has the variable exported.

Note this is only half of #1627: the CLAUDE_CONFIG_DIR half is already
covered by the open PR #1029, which this deliberately does not touch.

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

* fix(installer): honor CLAUDE_CONFIG_DIR for global Claude installs

Build on #1633 by @maxmilian and port the CLAUDE_CONFIG_DIR approach
from #1029 by @borfast onto the current installer. Keep the CODEX_HOME
cherry-pick cbb08231 intact.

Resolve non-blank Claude profile paths with path.resolve. Put the global
MCP JSON inside a custom profile while preserving ~/.claude.json for the
default profile. Settings, instructions, detection, and uninstall follow
the selected profile; local installs keep their existing paths.

Clear and restore CLAUDE_CONFIG_DIR in the setHome test harness. Cover
absolute and relative profiles, idempotency, unset/empty/blank fallback,
default-profile preservation, detection/uninstall, and local installs.
Combine the Unreleased note for both environment variables.

Thanks @seanchann for reporting the issue.

Validation on Linux with Node 22.19.0:
- npx tsc -p tsconfig.json
- npx vitest run __tests__/installer-targets.test.ts: 245 passed, 3 skipped
- Reproduced both failures against main e720f6ca; the rebuilt CLI writes
  all files into CLAUDE_CONFIG_DIR and CODEX_HOME with no ~/.claude,
  ~/.claude.json, or ~/.codex created.

Fixes #1627

---------

Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 12:17:06 -05:00
e720f6ca53 fix(extraction): preload the Objective-C grammar for C-family headers (#1628) (#1781)
Land upstream PR #1634 by @maxmilian, commit
8d398a92e30c9df27196bc90833efd674384569d.

Path-only detection classifies .h files as c, so preloading previously
covered c and cpp but missed objc selected by content-aware detection.
Preload objc alongside cpp whenever c is present. Full indexing and
changed-file reindexing now share preloadLanguagesForFiles().

Retain all four upstream unit tests and place the existing #1628 changelog
entry under Unreleased / Fixes / Symbols, tests and the viewer.

Verified fail -> pass on Linux with the reporter's repro.h as the only
source file, without a .m or .mm grammar seed. Before: Objective-C parser
initialization failure, 0 nodes, index state failed. After: Indexed 1 files,
2 nodes, 1 edge, index state complete; node CGRepro finds the class and
errors.log is absent.

Validation: npx tsc && npm run copy-assets passed; rebuilt CLI is executable.
npx vitest run __tests__/preload-languages.test.ts: 4 tests passed.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Max Hsu <maxmilian@gmail.com>
2026-09-08 11:51:02 -05:00
ee83636acb fix(extraction): index TypeScript interface members (#1638) (#1780)
Land upstream #1686 (maxmilian + bompus kernel/CG-28 follow-ups)
onto current main. tree-sitter-typescript interface members
(method_signature / property_signature) were never listed in the
TS extractor, so platform .d.ts APIs had no declaration nodes for
call edges. Mirrors on the Rust kernel path; keeps CG-28 damping
for pure-interface declaration files; filters damped files from
the explore RWR seed set.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 11:27:05 -05:00
8c047342cd fix(lua): index assignment-style function definitions (#1650) (#1778)
Apply upstream PR #1623 by danusha2345 (fix commit aa777063),
which also addresses #1616, to the current main base. Preserve the
upstream WASM and Rust implementations and regression coverage.

Index assigned locals, table members, static string keys and nested
callback tables as callable symbols, with calls owned by those symbols.
Keep dynamic keys unguessed. Add #1650 to the Unreleased changelog and
retain the existing re-index guidance without an extraction-version bump.

Verified on Linux x64 with Node 22.19.0:
- native kernel build, tsc, asset copy, executable CLI
- issue repro: 3 nodes / 2 edges -> 4 nodes / 4 edges
- EPR.PowerController::SyncHydroPower is indexed; its caller is client.lua
- syncHydroPower depth-2 impact reaches client.lua
- extraction/resolution/Lua parity: 844 tests passed (kernel expected)
- forced-WASM Lua/Luau extraction/resolution: 20 tests passed

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 10:50:09 -05:00
195888d71f fix(resolution): reject Python module collection method guesses (#1652) (#1776)
Use same-file module variable initializers to filter builtin collection calls before class-name heuristics, and require a Python type node for the class escape. Preserve imported project module calls.

Add callers/callees regressions for dict, list, set, tuple, and frozenset, with a real instance control and same-name bindings across files.

Validation on Linux with Node 22.19.0: the new suite had 14 failures and one passing control on main at 8733c288; all 216 tests in the new suite, call-receiver-no-fabrication, and resolution now pass. TypeScript and copy-assets pass. Re-indexed /tmp/cg-1652-repro: callers get is empty and read_setting no longer calls a cache.py method.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 10:29:53 -05:00
8733c2880f fix(extraction): index const-bound functions inside a body as symbols (#1669) (#1774)
`const handleClear = () => {…}` inside a component — every React handler
that skips useCallback — was never a symbol: the body walker only named
nested function declarations and hook-bound arrows, so the handler was
absent from callers/impact ("Symbol not found", indistinguishable from
"no callers") and its calls attributed to the component. Bind the arrow
or function expression to its declarator the way module scope already
does, in both the wasm walker and the kernel.

A navigation such a handler makes is now the handler's own edge and a hop
in the Screens `via` chain — the shape a useCallback handler already has —
so the react-router and expo-router expectations follow that convention.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 10:07:46 -05:00
85550eb2ce fix: report callers/callees/query truncation (#1674) (#1772)
Land #1647 onto current main: callers/callees/query (CLI + MCP) now say
when --limit hid matches, with totals in JSON and a widening hint. Also
covers #1639.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:52:56 -05:00
43271f3cd3 fix(extraction): index CommonJS export assignments as functions (#1675) (#1771)
`exports.getItems = async (req, res) => {…}` and `module.exports.x =
function () {…}` — the Express controller style — produced no symbol: the
arrow's parent is an assignment, not a declarator, so it stayed anonymous,
its calls attributed to the file, and `node`/`callers` answered "Symbol
not found" for a route-wired handler. Resolve the name from the export
property, mark it exported, in both the wasm walker and the kernel.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:37:20 -05:00
3adf06772b fix(resolution): read a quoted Python annotation as a receiver type (#1684) (#1770)
`def f(o: "Alpha")` is the same annotation as `def f(o: Alpha)` — a
forward reference, and what every file under `from __future__ import
annotations` writes — but the receiver-type pattern stopped at the quote,
read no type, and `o.render()` produced no edge. Admit the quoted form.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:30:01 -05:00
4105249843 fix(prompt-hook): cap injection under Claude Code's 10k inline limit (#1694) (#1769)
Claude Code persists hook stdout over 10,000 characters to a file and shows
the model a 2 KB preview. The prompt-hook MAX of 16,000 always hit that path
once explore filled the budget. Cap at 9,000 (exported + unit-tested) so the
payload lands inline, with headroom for the wrapper and projectPath nudges.

Lands the approach from #1695 with a testable helper.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:15:13 -05:00
Colby MchenryandGitHub b715eb6374 Merge pull request #1697 from bompus/fix/claude-always-load
fix(mcp): keep codegraph_explore loaded in Claude Code and Copilot CLI
2026-09-08 08:58:51 -05:00
a983a1bb68 fix(installer): write OpenCode 2 native MCP shape with codemode:false (#1698) (#1768)
OpenCode 2 exposes MCP tools through Code Mode by default; `codemode: false`
only survives on `mcp.servers.<name>` with `disabled`. The installer wrote the
v1 `mcp.codegraph` + `enabled` shape, so the opt-out was dropped on normalize.

Write `mcp.servers.codegraph` with `disabled: false` and `codemode: false`,
migrate a leftover v1 entry on re-install, uninstall either shape, and keep
printConfig / README / AGENTS.md in sync.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 08:41:01 -05:00
Colby MchenryandGitHub 28033f62f8 fix(resolution): an import naming the emitted .js extension resolves to its .ts source (#1767)
Fixes #1705. Lands #1706 (thanks @bompus), rebased onto main.
2026-09-08 08:00:52 -05:00
a7ea5ba730 fix(extraction): a TS/JS call through a host-global chain emits no ref (#1707) (#1766)
`chrome.storage.local.get(key)` and `document.body.querySelector(s)` end in
a platform API, but the extractor emitted the bare method name for them. That
name then exact-matched whatever project symbol shared it: in a Chrome
extension every `chrome.storage.local.get/set` inside a storage wrapper bound
to the wrapper's own `get`/`set`, giving self-edges that are not in the
source (#1707).

A member chain whose root identifier is a host object the project never
declares now emits nothing — a silent miss instead of a wrong edge, the same
trade the literal-receiver gate makes (#1230). `window` is deliberately not a
host root: `window.MyNs.doThing()` reaches a project symbol. A chain rooted at
a project value keeps the bare name, so `store.getState().act()`, `ref.value
.m()` and `this.<field>.m()` are untouched.

The Rust kernel mirrors the same gate. Verified on Linux: fail→pass on both
kernel and wasm arms for `__tests__/ts-chained-receiver.test.ts` (2 fail / 1
pass on main → 3/3 with the fix).

Lands / rebases https://github.com/colbymchenry/codegraph/pull/1710 onto
current main.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
2026-09-08 07:47:28 -05:00
Colby MchenryandGitHub 90dcdbc827 Merge pull request #1718 from danusha2345/fix/1708-fuzzy-reachability-on-survivor
fix(resolution): fuzzy reachability rejects a unique guess, never manufactures one
2026-09-08 07:33:49 -05:00
d983f73484 fix(explore): name symbols elided by a file trim (#1711) (#1765)
When codegraph_explore trimmed a rendered file, holes printed as bare
`... (gap) ...` and the header's `+N more` hid the dropped names — while the
footer asked the model to re-explore with exact names it was never given.

Name elided defs in gap markers as `name (file:line)` (using the full file
index, not just the relevance gather), bias the per-file header toward
symbols the trim cut, and point the trimmed-footer at those names.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 07:31:04 -05:00
Aaron Queen 097cd19ad2 fix(mcp): keep codegraph_explore loaded in Claude Code and Copilot CLI (#1696)
Claude Code defers every MCP tool behind ToolSearch by default, so a fresh session sees only the tool name until the model searches for it. The explore tool now carries `_meta: { "anthropic/alwaysLoad": true }`, which exempts it on existing installs, and the Claude Code installer target writes `alwaysLoad: true` on the server entry (re-running install adds the key to an older entry). Copilot CLI tool search holds MCP tools back the same way once ~30 tools are connected, so its entry carries `deferTools: "never"`.
2026-09-08 03:23:33 -06:00
danusha2345andClaude Fable 5.1 7c758aaf0a fix(resolution): C and C++ nesting is never a scope
isLexicallyReachable trusted the graph's nesting for every language. C and
C++ have no nested named functions, so a function shown inside another is an
extraction artifact: tree-sitter-c cannot parse a macro call whose arguments
are designated initializers — betaflight's

    RESET_CONFIG(pidProfile_t, pidProfile, .pid = { … }, …);

— and its error recovery runs the enclosing function_definition (source lines
168–309) to line 1667, nesting the 45 functions after it. That tree has 310
such functions in 73 files. Before this commit exact-match already rejected
them as unreachable and the fuzzy fallback picked them up at 0.5; with the
survivor-side check alone, fuzzy rejected them too and 117 real calls into
pid.c disappeared (base → 4c8f165 on the 2,109-file betaflight fork: LOST
117, GAINED 0, all fuzzy, all pid.c).

With the gate the same tree is LOST 117 fuzzy / GAINED 117 exact-match — the
identical edges, now resolved by the strategy that should have had them, at
0.9. vite (no C) is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:12:07 +03:00
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
Aaron Queenanddanusha2345 3d9352c7ac test(resolution): a builtin method call must not land on another file's closure
The two-file reachability fixture from #1709: a `function text()` nested in
one file, a `settled.value.text()` call in another. The caller must not get
a `calls` edge onto the closure; the in-container call still resolves.
2026-09-08 11:11:23 +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
2f1a99d34c fix(extraction): index C++ pure virtual methods as nodes (#1727) (#1758)
Pure-virtual declarations (`virtual int read(int key) = 0;`) parse as
field_declaration, not function_definition, so they minted no method node —
calls through an abstract base and cpp-override synthesis had nothing to
attach to. Mirror Java interface methods: mint the node (TS + kernel), mark
isAbstract, and cover with extraction/e2e/parity fixtures.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 02:04:12 -05:00
8df9ecac9d fix(extraction): blank C designated-initializer macro args before parsing (#1755)
tree-sitter-c has no rule for `.field = value` as a call argument. A
statement-level `MACRO(a, b, .x = …, .y = { … },);` recovers by extending
the enclosing function_definition to EOF — later functions vanish or nest
as outer::inner (#1729). blankCDesignatedMacroArgs empties such argument
lists to spaces (newlines kept) at the head of preParseCSource, before the
kernel route point, so both wasm and kernel C arms see the same bytes.

Tests cover the issue fixture (trailing-comma designated args) and a
120-field scale guard. Refs #1729.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 01:38:46 -05:00
9a32487491 fix(watcher): align ignore scope with git --exclude-standard (#1728) (#1754)
buildDefaultIgnore now reads .git/info/exclude and core.excludesFile; buildScopeIgnore also seeds directories git ls-files reports as ignored-untracked so nested .gitignore effects prune the live watcher. Defect B (full-project sync per event) was already fixed via scoped pendingFiles sync.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 01:19:05 -05:00
9b8bb4aba0 fix(db): fail closed when WAL valve cannot checkpoint past caps (#1539) (#1751)
sync() armed the WAL valve but never called backpressure(), so daemon
catch-up could grow the WAL without bound while query-pool readers pinned
frames. Wire the writer pause into sync store + batched resolution, and
abort with WalValveAbortError after parked backfills fail past the
documented hard/file caps instead of disabling parking for 60s.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 01:06:56 -05:00
bb204f8855 fix(extraction): diagnose git→FS scan fallback and lock nested .gitignore (#1567) (#1750)
Could not reproduce the reported nested-gitignore node_modules blowup on
Linux against main (or with the real Boba-Base ignore files): both the git
ls-files path and scanDirectoryWalk already exclude via DEFAULT_IGNORE and
per-directory .gitignore. Add CODEGRAPH_DEBUG logging when the git listing
falls back, plus regression tests for the reporter's layout on both scan
paths so a future regression fails loudly.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:53:09 -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
7440d2c475 fix(mcp): fail fast on a second direct-mode writer per project (#1740) (#1744)
Concurrent CODEGRAPH_NO_DAEMON / in-process fallback serve --mcp instances
each started a FileWatcher and contended on codegraph.lock until auto-sync
degraded. Add an exclusive .codegraph/writer.pid lock held by the daemon or
the single direct writer; a second writer exits with actionable guidance.
Daemon mode still multiplexes N proxies onto one writer.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-07 23:56:52 -05:00
df435d50d1 fix(extraction): index TS/JS generator function declarations and expressions (#1741) (#1743)
Tree-sitter kinds generator_function_declaration / generator_function were
missing from both the wasm and native kernel function-type lists, so
function* / async function* (and const g = function* () {}) produced no
nodes. Add the kinds on both paths and cover TS+JS declaration/expression
forms in extraction tests.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-07 23:43:33 -05:00
Colby McHenry 3298db1292 feat(steps): render fork decisions as points with per-arm edges and captions
Adds full support for decisions at forks in both the code graph and the UI. Key changes introduce a decision model for forks (innermost guard decisions), propagate decision data through the server and wire layer, and render decisions in the UI as distinct points with labeled arms. New components (ForkPoint and DecisionCaption) visualize the decision and its arms, while utilities (armWords, forkLabel) generate arm captions. The order reading (canvas) now shows decisions as points, and arms are drawn as separate edges (yes/no/case), with labels and captions displayed under the deciding box. Tests, typings, and docs updated to reflect the new decision visualization and behavior, including selection reach and resting-label semantics. This lays the groundwork for clearer visualization of conditional navigation and guarded branches on the order canvas.
2026-08-31 17:10:13 -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 Opus 5 209a07e881 feat(steps): the order reading is the canvas, not a rail
The first cut drew the code's order as a nested document — a column of boxes,
forks as rows of arm columns. Wrong picture: hard to read, and it threw away
the thing that made the tree legible. The ask was the canvas back, with the
timing fixed: the 200 comes after the token is signed, so it should branch out
of it.

So the order reading is now the SAME canvas, the same boxes, the same pills,
hover and panel — only the graph changes. `ui/src/lib/program-model.ts` walks
the server's block tree carrying a set of tails (the steps a next step would
follow) and emits one edge per "and then": proshop's login draws the anchor,
`User.findOne`, then the fork — `jwt.sign` under one arm with the `200` a row
below it, the `401` under the other. A row down is one more thing that has
already happened; an arm that answers, returns or throws has nothing leaving
it; a helper, a loop, `later` and `together` ride on the line into what they
hold. Rows are settled by relaxation, because a step reached twice can make
the graph cyclic.

A line means "and then" here and "leads to" in the tree, so the key says which.
The fork conditions are drawn at rest rather than only for a selected box —
`placeLabels` takes an `atRest` flag — because on this picture they are the
content, and two ways to one step merge as one condition (`WHEN userExists OR
NOT user`), not as two rendered labels stuck together.

`StepsRail.svelte` and `RailBlock.svelte` are gone; `StepBox.svelte` stays as
the box both readings draw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 14:16:01 -05:00
Colby McHenryandClaude Opus 5 676030314a fix(steps): three things the real repos caught
Validating the reading against four real servers turned up three defects, all
in the walk and all visible in both readings:

- **A hop's span is only a hop's span when the call is the one we asked for.**
  An inline Express handler's edges carry the ROUTE's line — where the only
  call is `router.post('/users/login', async (req, res) => {` — so the read
  span covered the whole registration and every call in the handler counted as
  written inside it, and so as running first. express-realworld's login drew
  its 200 before the `login()` that produces it. A read that does not find the
  call it was asked for is now a bare position: no span, no `inside`.

- **A name-match the call as written disproves.** `crypto.createHash('sha256')
  .update(…)` in a Nest service kept only `update` in the index and matched it
  to the caller's own `AuthService.update` — and the login endpoint then read
  as though it updated the user, four extra replies and a session delete
  included. In this family a method of your own class is written `this.x(…)`,
  so a receiver that is not `this` proves the guess wrong; the call leaves the
  index instead. The endpoint goes from 15 steps to 6, all of them real.

- **A value with no calls of its own is lent the file's.** The gate counted any
  edge, and `const signIn = validatedAction(schema, async (data) => { … })`
  holds one plain `references` edge to its schema — so the whole server action
  went unlent and its picture had one call out of nine. Only edges the walk
  follows as behaviour count now, and next-saas-starter's `signIn` reads whole:
  the lookup, two early returns, `Promise.all` of session and activity log, and
  the redirect to /dashboard or the checkout session.

Also: an `elif` whose body raises does not mean the arm it is written in always
raises — FastAPI's `if not user: raise … elif not user.is_active: raise …` was
ending its own arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:46:01 -05:00
Colby McHenryandClaude Opus 5 7b6704a70d feat(steps): a run of calls that happens once per item says so
A body drawn once, with nothing to say it repeats, is a quiet lie about the
order — so the reading now reads the loops a site is written inside, the same
way it reads its conditions: one climb up the same ancestors, per language,
`for` / `foreach` / `for … in` / `while` / `do` / `repeat`, with the header as
written (`item of items`, `queue.length > 0`) and where the loop starts.

Loops and forks nest in either direction, and neither reading knows about the
other, so the block builder merges them by where each construct BEGINS: on one
ancestor chain the outer one always starts first, which rebuilds the nesting
from the positions alone. A `for` inside an `if` and an `if` inside a `for` come
out the way the code has them.

With it, the per-framework readings are pinned: an Express handler with its
helper drawn inside the reply it builds, a FastAPI `raise HTTPException` ending
the arm it is in, a Spring early `return` as the other arm of its `if` (with
the comparison flipped, not wrapped), an ASP.NET handler's two outcomes, and a
Nest controller read on through the service it delegates to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:40:05 -05:00
Colby McHenryandClaude Opus 5 9acab0020f feat(steps): the rail — a handler read top to bottom, forks and all
The reading the walk records now has a picture. `#/steps?…&view=order` draws
the anchor, then its body: a box per step in the order the code writes them, a
fork where the code forks with its arms side by side under the condition, a
helper drawn where it is called, and an arm that answers, returns or throws
ending there — so proshop's login reads *look the user up · if the password
matches, sign a token inside the reply and answer 200 · otherwise 401*, which
is what the code says and what a row of four boxes could not.

- `program-model.ts` decides the words: the fork carries the decision once and
  its arms say only which side they are (WHEN / WHEN NOT), except a `switch`,
  whose arms each have a case to say, and a `try`, which says `on error` once.
- `StepBox.svelte` is the box both readings draw — the canvas wraps it in
  handles, the rail lets it size to its words. Same look, same click, same
  double-click-to-start-here.
- `StepsKey.svelte` is the key, floating over the canvas as before and last in
  the document on the rail, which scrolls and cannot have things sitting on it.
- The reading travels in the URL (`view=order` / `view=tree`) and the summary
  offers both; without one, the answer's own default decides — the code's order
  for a handler or an endpoint, the tree for a screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:35:17 -05:00