Two surfaces label a synthesized (dynamic-dispatch) `calls` hop:
codegraph_explore's Flow and dynamic-dispatch links plus the
codegraph_node trail (`dynamic: interface → impl @file:line`), and
ContextBuilder's "## Call paths", which `codegraph context` prints
(`→[…]`). Each kept its own list. The call paths' list stopped at
callback, react-render, jsx-render, vue-handler, http-client, queue-job
and socket messages, so every other synthesizer fell through to
`event ${m.event ?? ''}`: an interface-impl hop read
`→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux
thunk, a gin middleware chain or a closure collection.
The wording now lives once, in src/graph/synthesized-hop.ts
(`describeSynthesizedHop`: a summary, the detail only the call paths
print, and the wiring site), as AGENTS.md asks of a derivation two
surfaces render. ToolHandler renders `dynamic: <summary> @site`, the
call paths `<summary><detail> @site`. Explore and the node trail print
exactly what they printed before; the call paths keep their wording for
the kinds they already labelled, including the queue name, a socket's
direction and the HTTP clause, and now name the rest the way explore
does. A bus event reads `bus event` there too, and the section's
footnote no longer says hops are marked `[callback/event …]`.
The helper also holds the wording #2414 gives a Go method a struct gets
by embedding (`metadata.promotedInto`), so that arm moves here too. The
unused long `label` form of ToolHandler.synthEdgeNote (last read by the
removed codegraph_trace) is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explore's named walk is breadth-first, and the first callee listed claimed
each node it reached. An interface method calls every implementation at the
same depth, so when several of them call the next named symbol, the Flow went
through whichever implementation the index listed first, even when the query
named another one. On prometheus, `Engine.execEvalStmt Queryable.Querier
fanout.Querier NewMergeQuerier` went through the TSDB's `DB.Querier`; on gin,
once defined types count as implementers, `setter.TrySet` went through
`headerSource.TrySet` when the query named `formSource.TrySet`.
A node reached again from the same depth now takes the route that ends on
fewer unnamed hops, then the one through more named symbols, and keeps the
first otherwise. Of two equally deep named ends, the seed keeps the one whose
route passes more named symbols. The expansion order, the visited set and the
NAMED_VISIT_CAP bound are unchanged, so the walk makes the same callee lookups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's Dead code screen listed TypeScript interfaces that augment a
type declared outside the project: chart.js's `PluginOptionsByType` in
`declare module 'chart.js' { … }` (ghostfolio), `interface Window` in
`declare global { … }` (angular-realworld). The library or the runtime
reads them through the type they merge into, so nothing in the repository
names them, and the index recorded them as unexported file-level
interfaces.
TypeScript exports what such a body declares without the keyword. The
binder makes an ambient module body an export context unless it holds an
export declaration (`export {}`, `export =`, `export default x`), and a
namespace nested in one is ambient too. Both extractors now set isExported
that way: isAmbientExport in languages/typescript.ts, shared by TSX and
ArkTS, and is_ambient_export in the kernel's tsjs walker. The exported rule
then drops them, counted under `exported`, and that count's label now
names them. `declare namespace X` is unchanged: in a module file X belongs
to that file.
The resolver sees them as exported too. A file holding `declare global`
was already treated as exporting, so nothing moves there. A `declare
module` member becomes reachable across files by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's Dead code screen listed two kinds of framework-called methods.
A method whose decorator resolves outside the project (`@HostListener`,
`@Cron`, `@OnEvent`, `@Process`, `@BeforeInsert`, `@InitBinder`) has no
`decorates` edge and no `node.decorators`, only the resolver's unresolved
`decorates` row. That row now counts as a decorator, the same evidence the
opaque-ancestor rule reads for an `extends`/`implements` row, under the
existing `decorated` count.
A lifecycle hook on a class that skips the `implements` clause (`ngOnInit`
on a `@Component` without `implements OnInit`) had no evidence at all. A new
`hooks` rule drops a TypeScript method when its class carries a decorator and
an interface outside the index carries the name. What the interface carries is
read off the classes that write it out: every one has the method, declared or
inherited, at least two and more than half declare it, and more than half of
them carry a decorator. TypeScript only, because `implements` is erased there
and a framework can only be finding the hook by name.
The ancestor walk moves out of overrideCandidates into ancestorsOf, shared by
both rules. Declarations are read from the candidate list rather than searched
by name: a hook is never called from inside the index, and a name some
declaration of which is called is the ambiguous-name rule's anyway.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`codegraph explore MYCOPYBOOK`, the `codegraph_explore` MCP tool and
`codegraph context` answered "No relevant code found" for a COBOL copybook
named in the query, while `query`/`node` found its include at once.
Cause: the COBOL extractor records `COPY X` and `EXEC SQL INCLUDE X` as
`import` nodes, and findRelevantContext filters every channel by
HIGH_VALUE_NODE_KINDS, which leaves imports out on purpose (a JS/TS/Python
import statement is noise in explore). With the include out of reach the
text search fell through to its fuzzy fallback, so a member with a digit in
it (never extracted as a symbol) came back as an unrelated program one edit
away: CVACT01Y -> CBACT01C, CVCUS01Y -> CBCUS01C on CardDemo. Making COBOL
imports "high value" alone would not have helped either: explore's renderer
skips import nodes, and the import-to-definition step drops a COBOL include
because its `imports` edge leaves the enclosing scope, not the import node.
Change:
- src/graph/cobol-copybooks.ts (new, shared derivation): resolves the COBOL
words of a query to copybooks - every COPY / EXEC SQL INCLUDE of the member
(case-insensitive) and the indexed copybook file (.cpy, or a file an
include resolved to; a same-named program is not one). Gated on the
project having COBOL files at all (one seek on idx_files_language), so no
other language's query does extra work or changes. Plain lowercase words
in a multi-word query are English, not member names.
- findRelevantContext: a named copybook's top-level declarations, then its
include statements, become entry points ahead of the kind-filtered
channels, like an exact filename (at most half the slots when the query
matched other things, include sites in those files first). They are not
walked (BFS from a record layout spent the answer on whichever program
MOVEs into it), the member is not re-searched as text (the fuzzy decoy),
and an explicit nodeKinds filter without `import` is honored.
- codegraph_explore: the copybook file is pinned like a path in the query;
an include statement that is an entry point is grouped and rendered (a
window around the COPY line) instead of skipped; a new "COBOL copybook"
section lists every include site (path:line, COPY vs EXEC SQL INCLUDE,
25 lines then a count) or says the member has no indexed source (DB2
DCLGEN, compiler-supplied such as SQLCA/DFHAID).
- QueryBuilder: three indexed lookups; CodeGraph.findNamedCopybooks exposes
the derivation so explore and the context builder share one answer.
No extraction change; no re-index needed.
Verification: __tests__/explore-cobol-copybook.test.ts (8 tests, through
ToolHandler codegraph_explore and buildContext): with src/ reverted to
origin/main 6 fail (the 2 guard cases pass on both), 8/8 pass with the fix.
tsc clean, npm run build OK. Issue repro fixed on the built CLI (explore +
context) and through a real `serve --mcp` tools/call. Full suite on a heavily
loaded box: 5716 passed, 25 failed in 15 files; 12 of those files pass alone,
and the other 3 (function-ref, git-index-currency, worktree-detection) are
5 s / 60 s timeouts that fail on clean main as well in interleaved runs.
Validation (both builds on one index per repo; query-side change only):
- expressjs/express: 14 explore + 4 context queries byte-identical.
- aws-mainframe-modernization-carddemo: 10 non-copybook queries
byte-identical; 15 copybook queries all now return the copybook (when
indexed) and every include site - before: 9 "No relevant code found",
4 unrelated programs, 2 without the copybook.
- cicsdev/cics-genapp: 6 non-copybook queries byte-identical; 6 copybook
queries fixed (LGCMAREA's 25 sites incl. multi-line EXEC SQL INCLUDE,
lowercase lgpolicy, SSMAP with no indexed source).
- In-process explore latency on express unchanged within noise.
Thanks @popolusiak for the report.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's server classifies a code slice with the engine's tree-sitter
grammars on its one thread. The COBOL grammar's scanner loops forever on a
line like ` .`, which a cobolcraft Flow card's window ends with, so the
whole server froze — /api/stats included — and a parse inside WebAssembly
cannot be interrupted from JavaScript. Slices are now classified in a
worker that is terminated past a 2 s deadline; the slice is served plain
and the next one gets a fresh worker. Request-time parses also carry a
progress-callback budget.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Flow strip's from/to were run through explore's free-text token filter,
which drops an Objective-C selector (`initWithFrame:`), a Ruby predicate
(`valid?`) and any name under three letters (`ok`) — then reported the
OTHER end as naming nothing in the index. Directed mode now takes the two
names as written; explore's named mode is unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found testing `codegraph ui` on Spring and Laravel: a server-rendered
endpoint's Steps picture never said how it answers. petclinic's POST
/owners/new showed its database write and no reply; BookStack's book page
showed a 404 and not the page it renders.
Spring MVC answers by what a handler RETURNS, which is no call:
- A new tree-sitter reader, returnsInTree / returnsForFile (graph/
branch-guards.ts, exposed as SiteReader.returns), lists the returns a
definition makes itself — never one in a lambda, a local function or a
nested / anonymous class — and the string a same-class constant holds.
- steps.ts: for a mapped method of a class that renders views (not
@RestController, not @ResponseBody), a returned view name / constant is a
200 render, `"redirect:…"` a 302, `"forward:…"` a forward, each under the
condition it is returned in (effectLink takes a preset effect for a site
that is no call). Non-literal returns are not guessed at.
- effects.ts: `new ModelAndView(…)` (200, or 302 for "redirect:…") and
`new RedirectView(…)` (302) are replies.
Laravel's `view(…)` and `redirect(…)` were already reply rules, but never
reached them:
- name-matcher.ts: a PHP call written without a receiver is a function
call, so only a `function` is a candidate — BookStack's every
`return redirect(…)` bound to ApiDocsController::redirect, every
`view(…)` to a `$view` field (and, through the fuzzy fallback, to the
class `View`). PHP refs record the call expression's column, so the call
text at that column decides. koel alone loses 60 such wrong edges
(`basename()`, `auth()`, `value()`, …).
- effects.ts: the PHP reply rule never matched a chain — rules see the text
with argument lists stripped (`response->json`), the rule wanted `()->` —
so `response()->json(…)`, `redirect()->route(…)`, `redirect()->back()->
withErrors(…)` were never replies; `to_route` added; redirect chains are
302s. PHP's SPL programming-error exceptions (InvalidArgumentException,
LogicException, …) are no longer replies, and a namespaced exception is
read by its class name.
petclinic POST /owners/new: 200 `view owners/createOrUpdateOwnerForm` WHEN
result.hasErrors(), 302 `redirect:/owners/${…}` WHEN !result.hasErrors().
BookStack GET /books/{slug}: 404, 200 `view`, 302 `redirect` (the
InvalidArgumentException from a helper is gone). realworld's REST login is
unchanged.
Not in scope: PHP has no site reader (no branch-guard rules), so Laravel
replies carry no WHEN and no arguments — `abort(Response::HTTP_…)` and
`response()->json()` show without a status.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Path search marked nodes visited only on dequeue, allowing pending targets to be queued repeatedly with copied paths.
Track enqueued nodes from the starting node and exclude them from target lookups and subsequent pushes.
Add real-SQLite operation-count and path-correctness regression coverage.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dead-code): treat an ancestor outside the index as opaque (#1973)
An override is excluded from the dead-code report when its ancestor is in
the graph, or is present but declares no members we can read. An ancestor
outside the project (React.Component, stream.Transform, a NestJS
interface) creates no edge at all, so a lifecycle method the framework
calls looked like it overrode nothing and was listed as unreferenced.
The resolver does record such an ancestor, as a failed extends /
implements row. A container with one, or a project ancestor of one that
has it, now counts as having an opaque ancestor, and its members are
excluded under "overriding".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dead-code): propagate external ancestor uncertainty to descendants (#1973)
External ancestors leave unresolved inheritance references rather than graph edges.
Treat those ancestors as opaque and propagate uncertainty through every descendant root.
Track visited ancestor/root pairs while preserving cycle protection and the depth bound.
Cover direct and three-level inheritance, unused controls, and batched query behavior.
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
getImpactRadius, getCallers and getCallees are depth-limited DFS walks
with one visited set. A node first reached through a longer path at the
depth limit was marked visited without being expanded, so when a shorter
path reached it later it was skipped and its own dependents within the
limit were lost. Record the shallowest depth each node was expanded at
and expand it again when a nearer path reaches it. Results and edges are
still reported once, and output order is unchanged where nothing was
missing.
(cherry picked from commit ec56377b37)
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adapt @uvmplus's PR #1481 (3ecf7479) to the shared symbol lookup and
named-symbol flow resolver on current main. Missing names return not found
with suggestions, and exact matches with no callers stay empty.
Preserve #1512 definition grouping and --file narrowing, #173 qualified
misses, and codegraph_node's intentional fuzzy file lookup. Port the
upstream regression suite and cover the moved shared lookup paths.
Validation on Linux with Node 22: project build, 74 requested tests,
47 related flow tests, and 16 same-fixture CLI/MCP checks pass. Baseline
captured 12 failing tests and 13 failing fixture checks.
Fixes#1473.
Supersedes #1481.
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Squash danusha2345's PR #1511 at d282f9e8 onto main 8c9c4761,
preserving its nine non-merge commits and main's existing Unreleased notes.
Calls in Kotlin, Java, TS/JS, Scala, Rust and Python declaration initializers
now retain the owner established by the upstream regression expectations.
Include the upstream CFML, dynamic-dispatch summary and viewer follow-ups.
Linux fail-to-pass validation (Node 22.19.0, rebuilt dist and native kernel):
- Before: TS load belonged to file:app.ts; Python/Kotlin/Scala/Rust calls
vanished; Java lost the field-lambda, anonymous override and eager calls.
- After: all six languages PASS; 12 native/WASM LF/CRLF parity checks PASS.
- Focused initializer regressions: 10 passed with CODEGRAPH_KERNEL=0 and
10 passed with the kernel enabled; Kotlin's grammar fallback is recorded.
- Related regression suites: 879 passed, 1 skipped across 15 test files.
- Evidence: /workspace/cg-1510-repro/before and /workspace/cg-1510-repro/after
(combined test output: after/vitest.log).
Fixes#1510
Supersedes #1511
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
* fix(cli): port upstream symbol lookup consistency (#1656, #1512)
Port ferrine/fix/symbol-lookup-consistency at
c0ccbacd3f onto current main.
Qualified CLI queries use the shared matcher and ambiguous names disclose
their targets. Keep total/limit/truncated and the human truncation notice
from #1674, and share the matcher with main's named-symbol-flow module.
Refs #1512. Upstream PR: #1656.
Co-authored-by: ferres <justferres@yandex.ru>
* fix(cli): group traversal results by definition (#1512)
Extend upstream PR #1656, ported in 7038fb4f, so callers/callees/impact
show separate sections for each definition and accept --file using the
same groupDefinitions helper as MCP. Preserve same-file overload groups,
path/suffix matching, and the explicit fallback when no file matches.
JSON definitions carry their roots, own neighbors/affected nodes, and
edges. Retain the legacy top-level lists as an explicitly labeled union
and preserve #1674 total/limit/truncated; each callers/callees definition
also reports its own limit and truncation metadata.
Validation: npm run build; 141 tests across 14 targeted suites, including
32 CLI regression tests. The Linux /workspace/cg1512-repro failure now
passes for all three commands, with and without --file.
Fixes#1512.
Upstream PR: #1656 (ferrine/fix/symbol-lookup-consistency @ c0ccbacd).
---------
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: ferres <justferres@yandex.ru>
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
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
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
- 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
- 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
- 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
- api/route-roots.ts: the symbol a route runs (references-edge handler, exported page component, or the route itself for an inline handler), shared by steps and screens; the bare Steps tab lists an API's endpoints by router file
- api/effects.ts: database / response / queue / email / payments / cache / auth / process / network / storage / device / telemetry, matched on the call as written per language family, with model + read/write and the literal status on a response site
- graph/branch-guards.ts: callSitesForFile (the whole member chain), memberTypesInTree, decoratorsForFile, request/decorator triggers with the middleware/guard chain; guard + argument rules for Python, Java, Kotlin, C#, Go and C
- steps.ts: classify on the chain before trusting a name match, retarget this.x.y() by declared type, skip test doubles after the effect pre-check, project kind on the wire
- viewer: kindWord/kindWords per project kind, endpoint chooser, response boxes labelled by status codes
- python.ts: FastAPI detected from a monorepo sub-directory; is-test-file: samples/examples package paths are not tests
- tests: ui-steps-api-servers, ui-effects, branch-guards-languages; spec §3.13 Servers paragraph, CHANGELOG, plan doc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
- Introduces trigger metadata for steps and edges to capture what fires a site (JS prop, on* option, or callback) to improve cross-boundary flow analysis.
- Extends parsing/analysis to detect triggers in JSX attributes, on* bindings, and late-bound callbacks; adds utilities (calleeText, lastSegment) to extract trigger sources.
- Ships new trigger structures (WireStepTrigger, trigger on WireStepSite/WireStep) and propagates trigger through built steps; updates step labeling to reflect trigger information.
- Adds triggerWords helper and uses it to render human-readable trigger descriptions in Steps UI, including edge labels and per-site visuals.
- Updates UI (ScreensView, StepsView) to display FIRES FROM information, with styling tweaks to highlight triggers and related elements; enhances tooltips and inline text wrapping for readability.
- Extends tests to cover trigger detection and rendering across various binding patterns (prop, option, callback) and inline RN listeners.
- Updates design/docs and changelog to reflect Expo Router integration, per-site trigger metadata, and the new Steps surface.
- Adds Expo Router integration with a new Screens view and a Steps API to surface screens and their transitions.
- Extends codegraph extraction/resolution to handle namespace objects, React hook bindings for handlers, and Swift RN bridge evidence; introduces per-site guard arguments and trigger metadata, enabling richer flow analysis across JS ↔ native boundaries.
- Introduces UI and data-model changes to represent conditions as words (WHEN/AND/OR/NOT), display per-site call arguments, and show what fires a site (triggers). Adds new utilities (ui/conditions.ts) and updates ScreensView and StepsView to render scenarios with multiple sites and “ways” counts.
- Implements site readers for WHEN/ARGS/TRIGGER, and wiring to expose steps via API endpoints (including /api/steps); enhances tests to cover namespace resolution, useCallback-driven handlers, and inline RN event listeners.
- Updates styling and templates to reflect the new wording, scenario rows, and per-site details, including NOT instead of leading negation strings and multi-way links.
- Documents and reflects changes in changelog and design docs to describe Expo Router integration and the Steps surface.
Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps.
A Dead code screen and a mark on the Map, both drawn from one derivation in
src/graph/dead-code.ts so a second surface can never disagree with the first.
The SQL half is four lines — no incoming edge but `contains`. It returns ~2 500
candidates on this repository and the shipped list is 20; everything in between
is the feature. A candidate is dropped the moment there is any reason to believe
something outside the graph reaches it: exported symbols and header
declarations, test and generated files, abstract and interface members, anything
carrying a `decorates` edge, overrides of an ancestor's member, names the
language calls by itself, vendored directories, files nothing in the index
reaches (those are islands, and the Map says so instead), names the resolver
failed to resolve somewhere, and names shared with a symbol that IS referenced —
the mis-resolution that leaves a used method with a self-edge and its twin with
nothing. The last rule is the only one that is not a graph query: before a claim
is made, the declaring file and every file that reaches it are read and the
identifier counted, which is what catches the references the extractor never
recorded (`this.handleMessage.bind(this)`, a call inside an object literal, a
shorthand property).
Every subtraction is counted and printed under the list with the scale it came
from, and the caveat line above it never collapses: the claim is "no static
reference in the index", not "unused".
On the Map a module nothing depends on keeps its stroke and says so in its count
line, and tool-generated files and modules recede to ink-4 there, in the map's
file list, in search results and on the file screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A vertical tree above the members outline for classes, interfaces, structs,
traits, protocols, enums, unions and type aliases: ancestors above (the whole
chain, not just the direct parent), the focus in accent, subtypes below indented
per level. `extends` draws solid, `implements` dashed; a synthesized edge — Go's
implicit interface satisfaction — draws dashed wider and carries the site it was
wired at, so a relation the resolver inferred never reads like one the source
wrote down. For an interface the fan below IS the set of runtime targets a call
can land on, and a type with eight or more implementers leads with that in a
sentence. Members that redeclare an ancestor's are marked in the outline.
The walk lives in `src/graph/type-hierarchy.ts`, following CG-50/CG-51: shared
computation in `src/graph/`, presentation in the caller. Its `countImplementers`
is now also what `ToolHandler.buildPolymorphicBoundaries` counts with, so "N
types implement X" is the same N whether an agent reads it or a person does.
`/api/node` carries the block as `hierarchy` rather than a second endpoint —
it is part of the Symbol view's first paint, and gated to types, so a function
costs one kind test.
Layout is arithmetic (24px rows, 22px indent, orthogonal connectors computed
from the two): no ResizeObserver, same payload → same picture. The header's
`extends X` / `implemented by …` chips are suppressed while the tree is on
screen — two renderings of one relation in one column is how a reader ends up
trusting neither.
`TypeHierarchy` is exported from `@colbymchenry/codegraph-ui` and takes its data
as a prop, so a host holding a `WireSymbolPayload` renders it without a second
read.
A flow that does not reach what it was asked about now ends in a cap instead
of in silence: the dispatch form that ended it, the line, the static key when
the source spells one out, the candidate runtime targets as clickable rows,
and the name-only matches under 0.6 the search refused to follow. A flow that
does reach its destination never shows one.
The verdict is lifted out of `ToolHandler` into
`src/graph/dynamic-boundary-report.ts` and both callers render it —
`codegraph_explore`'s prose and `/api/flow`'s `WireFlowBoundary` — the same
move `named-symbol-flow.ts` made for the path finder, and for the same reason:
a reader holding the strip and the MCP answer must not be told two different
things. The explore prose is unchanged, byte for byte.
When nothing connects at all and a dispatch site explains why, the strip is
that site: one card opened at the line where the static path ends, plus the
cap. When nothing explains it, no stopping point is invented.
Ask "how does execute reach getFile" in the search box and the viewer draws the
call path between them, left to right, opening every card at the exact line that
makes the next call. Dynamic-dispatch hops are dashed and name the site they
were wired at; "Read as flow" turns a trail walked by hand into the same strip.
The path finder is NOT new. `codegraph_explore` already leads its answers with
the longest call chain among the symbols an agent named, and a viewer that drew
a different path would get the two quoted against each other in a review. So the
search moved out of `ToolHandler` into `src/graph/named-symbol-flow.ts` and both
callers ride it — same tokens, same overload rules, same synthesized edges. What
stayed behind in `tools.ts` is the prose.
A pinned from/to question is the same search with two options changed, because
both ends being named is the evidence explore's one-unnamed-bridge cap stands in
for: it bridges freely, keeps twelve candidates per endpoint instead of six
(the CLI's own `main` sorts seventh of ten), and searches from both ends at once
— identical paths to the one-way walk on twelve measured pairs, 3-6x faster.
`/api/flow` is deliberately the one endpoint with no cache: its cards carry
source read from disk, and a drift verdict changes without the index changing.
Verified on this repo (`execute` to `rowToFileRecord`, 8 hops; `main` to
`resolveOne`, 7) and on a fresh excalidraw index, where `mutateElement` to
`renderStaticScene` crosses callback, react-render and jsx-child hops and lists
exactly the hops `codegraph_explore` prints.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three root defects in src/graph/traversal.ts (reported by @inth3shadows as #1086–#1090):
- Depth guard returned before visited.add → duplicate callers/callees at maxDepth=1 and getImpact loop disagreement.
- Dedup gate also gated edge collection → traverseBFS dropped a parallel edge; getImpact dropped a direct incoming dependency edge.
- limit checked per-frame not per-add → high-degree node overshot opts.limit in traverseBFS and dfsRecursive.
traverseBFS now collects every distinct edge among kept nodes (deduped on edge identity), enqueues each node once, and caps per-add. getCallers/getCallees/getImpactRecursive mark visited before the depth check; getImpactRecursive records the incoming edge unconditionally and unifies its loops on visited. 7 regression tests in graph.test.ts, each failing on the pre-fix code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`callers <Class>` returned "No callers found" (or only the importing
file) even when a class's constructor was called from many sites, and
the instantiation sites were invisible — the opposite of what "what
breaks if I change this class?" should answer.
The `instantiates` edges already existed in the graph, correctly
attributed to the constructing function; they were simply excluded from
the caller/callee traversal, which queried only calls/references/imports.
Constructing a class is calling its constructor, so add `instantiates`
to the edge-kind set in both getCallers and getCallees (kept symmetric so
they stay inverses and `trace` can cross the instantiation boundary,
function -> class -> its methods). impact already traversed all edge
kinds, so it was unaffected.
Query-layer only — existing indexes benefit on upgrade with no re-index.
Verified on a Python fixture: `callers Supervisor` now returns the
construction sites (main/work/test_it), and a new graph test asserts
main() <-> DerivedClass via the instantiation. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the cross-file dependency graph behind impact / affected / explore across all 22 supported languages and 14 web frameworks, validated on real-world repos (measured fair-coverage table added to the README). Per-language resolution + framework resolvers/synthesizers (Lua/Luau require, Shopify OS 2.0 Liquid sections, Delphi forms, Rust cross-module + Rocket macros, Swift Fluent, SvelteKit/Nuxt loader/component conventions, RN/Expo bridges). 0 cross-family false edges, full suite green (1187 passed). See #708.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch of small, localized fixes from an open-issue triage:
- .codegraph/.gitignore now ignores everything but itself, so the database,
daemon.pid, sockets, and logs stop showing up in git status (#492, #484)
- MCP server answers resources/list and prompts/list with empty lists instead
of -32601, clearing scary log lines in opencode/Codex (#621)
- index SAP HANA .xsjs/.xsjslib as JavaScript (#556) and TS .mts/.cts (#366)
- visit anonymous AMD/CommonJS/IIFE wrapper bodies so their inner functions and
calls are indexed instead of coming up empty (#528)
- batch the changed-file lookup so a huge first sync no longer hits
"too many SQL variables" (#540)
- list files with `git ls-files -z` so non-ASCII/CJK paths survive
core.quotepath and are no longer silently skipped (#541)
- attach Go methods on generic receivers (*T[P]) to their type (#583, RC1)
- impact no longer climbs the structural `contains` edge, so a leaf symbol
stops dragging in its sibling methods (#536)
- README: explicit `codegraph install` step, run in a new shell (#631)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch getNodesByIds to collapse N+1 reads in graph traversal, invalidate the
insertNode LRU cache so INSERT OR REPLACE doesn't serve a stale row, and run
incremental PRAGMA optimize + passive WAL checkpoint after bulk writes.
Closes#108
Changes search result deduplication to use max scores across channels instead of first-seen prioritization, adds template component usage extraction for Svelte files, exempts exact matches from single-term score dampening, prioritizes structural edges in graph traversal, and increases explore tool node budget while including edge source locations in file clustering.
Three issues discovered testing CodeGraph against a Shopify Liquid theme:
1. Callers/callees only traversed 'calls' edges, missing 'references' and
'imports' edges that Liquid extraction creates for {% render %} and
{% section %} tags. Expanded edge filter in getCallers/getCallees.
2. Context builder only ran text search as a fallback when semantic search
returned nothing. For template-heavy codebases, semantic search returns
irrelevant results (e.g., "Toast" for a header navigation query) while
text/path-based matching would find the right files. Now always runs
text search alongside semantic search with multi-term boosting.
3. MCP findAllSymbols only matched nodes by exact name, missing file nodes
whose basename (without extension) matched the symbol. This caused
callers to find zero results even with correct edges, since references
edges point to file nodes (e.g., "product-card.liquid") not component
nodes (e.g., "product-card").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Java extraction:
- Handle Java method_invocation AST (receiver.method pattern)
- Support Java extends_interfaces and super_interfaces with type_list
- Create unresolved references for Java imports for cross-file resolution
- Extract interface inheritance via extractInheritance
MCP tools:
- Aggregate callers/callees/impact across ALL matching symbols (e.g. multiple
overloads or same-named methods in different classes)
- New findAllSymbols() helper for multi-symbol lookup
Graph traversal:
- Impact analysis now traverses into container children (class → methods)
so that callers of methods appear in the impact radius of their class
Other:
- Add deleteSpecificResolvedReferences() for precise cleanup after resolution
- Add 'instance-method' to resolvedBy union type
- Version bump to 0.6.8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Traverses dependency graph to identify which test files depend on changed source files. Supports stdin input for git integration, custom test file patterns, and configurable traversal depth. Useful for targeted test execution in CI/CD pipelines.
- Fix arrow function extraction: explicitly call extractFunction() for
arrow functions/function expressions in variable declarations instead
of silently skipping them (all 6 arrow function tests now pass)
- Best-candidate resolution: collect candidates from all strategies and
return highest confidence match instead of first match
- Fix graph traversal 'both' direction: correctly determine next node
for mixed incoming/outgoing edges in BFS and DFS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>