Commit Graph
511 Commits
Author SHA1 Message Date
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
Colby McHenryandClaude Opus 5 b02e192ffa feat(steps): the same walk, read in the code's order
The picture answers "what does this set in motion", a row per distance from
the anchor. On proshop's login that puts `User.findOne`, `jwt.sign`, `200` and
`401` side by side — all one step out — when the code says: look the user up,
then IF the password matches sign a token and answer 200, ELSE answer 401. The
signing is not beside the 200, it happens INSIDE the reply it is part of.

So the walk now records what happens in each function where the code writes it
— the step reached (or the helper folded into), the call's position and span,
and the branch guards, structured — and `api/program.ts` folds those records
into the anchor's body: items in source order, a fork wherever two sites are
arms of one decision, a helper drawn in place at its call, an arm that answers
the request or leaves ending there. A call written inside another call's
arguments comes first, so the token is signed before the reply that carries it.

It is a derivation, not a second walk: the records are made by the pass that
makes the links, so the two readings can never hold different steps. A fork
exists only where a guard was READ — a language without rules, or a file that
changed since the index, reads as a plain sequence rather than an invented
structure. The reading opens each function once, however many times it is
called (`again`), and is capped like everything else here.

The payload carries it as `program`, with `defaultView`: the code's order for a
handler, an endpoint or any function; the tree for a screen, where handlers
fire on events and have no order between them.

Measured on a 87-step / 160-link screen: +3% wall clock, +95 KB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:20:01 -05:00
Colby McHenryandClaude Opus 5 482690b62d feat(steps): guards say which decision they belong to, and how an arm leaves
A joined `when` string cannot tell an `if` from its `else`: two sites read as
opposite conditions, and nothing says they are the two arms of ONE decision.
The reading a rail needs is the structure, so each guard now carries it:

- `branch` — where the branching construct starts (`line:column`). Both arms of
  an `if`, every case of a `switch`, an early exit and the code it guards share
  it; two `try`/`catch` blocks in one function no longer collapse into one.
- `armExit` — how the arm the site is in leaves, when it always does (`return`,
  `throw`, or `exit` for a `panic` / `exit()` the rules count but no keyword
  names), read from the arm's last statement.
- `exit` — for an early exit, how the arm that was NOT taken leaves.

`SiteReader.guards()` returns the array; `when` is now `guardLabel` over it, so
a caller that wants both pays for one read. Nothing else changes: `guardLabel`
ignores the new fields and every existing label is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:08:41 -05:00
Colby McHenryandClaude Fable 5 783f3954ec feat(steps): rows read in the code's order; a hop written inside another call says so
- branch-guards callSiteInTree: a call's span and the call it is written inside the arguments of (`within`), stopping at a function or block boundary
- steps.ts: each step records the hop that first reached it (position, span, enclosing call — the fold's first hop out of the root, inherited down the fold); a row is ordered by that position, a hop inside another site's arguments before that site, and `WireStep.order` carries it; links carry `within`
- map-model: an `order` option — the row's initial order, sweeps over parents only, tie-broken by it; the Map and Screens tabs pass none and are unchanged
- viewer: rows laid out by `order`; `inside res.json(…)` in the panel rows and the tooltip
- tests: servers fixture (a token signed inside the reply's arguments: `within`, and the row `create · queue · mail · jwt.sign · 201`), model row order; spec §3.13, CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 12:25:27 -05:00
Colby McHenryandClaude Fable 5 46e3e7aaa0 feat(steps): one reply box per outcome
A reply's identity is its status, not its call: a handler answering 200 or 401 draws two boxes (id per function, response, status), so each line from the handler carries its own condition on the picture — the Screens view's idiom — and the anchor's Leads-to list reads as the contract; replies whose status the code does not spell out share one box labelled by the call. Panel note, spec §3.13, CHANGELOG, plan; servers test asserts the ASP.NET and Spring outcomes per box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 12:11:40 -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