R has no declaration syntax — everything is an expression — so the
extractor works through the visitNode hook: functions in every
assignment form (incl. nested, attributed to their enclosing scope),
top-level variables/constants, library()/require() imports and
source() file references (claimed, Lua-style), S4/RefClass/R6/ggproto
classes with their methods and extends edges, setGeneric/setMethod.
Grammar vendored from r-lib/tree-sitter-r v1.2.0 (ABI 14; npm package
is a security placeholder, tree-sitter-wasms has no R).
Benchmarked on AnomalyDetection (8/8 named defs), dplyr (1027 fns),
ggplot2 (150 ggproto classes / 597 methods / 128 extends edges —
adding ggproto mid-bench flipped the large-repo A/B from a regression
to 2.4x faster than the no-codegraph arm).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The shipped grammar parses every record form as record_declaration (no
record_struct_declaration node), so 'record struct' mis-kinded as class.
classifyClassNode now distinguishes the value-type form by its struct
keyword child, and extractStruct accepts bodiless positional records
(the no-body gate is for C/C++ forward declarations) instead of
crashing mid-file on them.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A workspace holding several independent git repos failed two ways:
- Enumeration: a super-repo whose .gitignore hides its child repos
(/packages/) indexed 0 of their files — git never lists ignored dirs,
and the #193 embedded-repo recursion only fired for UNTRACKED dirs.
Gitignored embedded repos are now discovered (ignored-dirs listing +
bounded .git search) and enumerated by their own git ls-files.
- Change detection: git status in the parent says nothing about embedded
repos (untracked OR ignored), so codegraph sync missed every child
change. Status now recurses per embedded repo.
ScopeIgnore is the new single source of truth for indexer + watcher
scope: parent rules for ordinary paths, the child repo's own rules for
paths inside it, built-in defaults uniformly on full paths (a git repo
inside node_modules is an npm git-dependency, not project code), and
ancestors of embedded roots are never pruned (the Linux per-directory
watcher must descend to reach them).
Non-git workspace roots already worked via the per-directory gitignore
walk — locked in by test.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): opencode global config goes to ~/.config/opencode on every platform (#535)
opencode resolves its config dir with xdg-basedir (XDG_CONFIG_HOME ??
~/.config) unconditionally — it never reads %APPDATA%; that layout
belonged to the discontinued Go fork. Writing there on Windows meant
opencode never saw the MCP entry.
- globalConfigDir(): drop the win32 APPDATA branch; XDG resolution everywhere
- install/uninstall (global): sweep a stale codegraph entry + AGENTS.md block
out of the legacy %APPDATA%/opencode location (siblings/comments untouched)
- detect(global): a legacy-only dir still counts as installed so the sweep
is reachable
- tests are env-gated, not platform-gated, so the whole matrix runs on any
OS; the suite previously pointed APPDATA and XDG_CONFIG_HOME at the same
dir, which is exactly how the divergence stayed invisible
Supersedes the prefer-if-exists approach of #670 (greenfield installs --
before opencode's first run -- would still have fallen back to APPDATA).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(installer): match legacy sweep paths by dir prefix, not 'AppData' substring
On Windows os.tmpdir() lives under AppData\Local\Temp, so every harness
path contains 'AppData' and the substring assertions false-positive.
Caught on the real Windows VM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(explore): announce dynamic-dispatch boundaries when a flow can't connect statically (#687)
When buildFlowFromNamedSymbols can't connect the agent's named symbols, scan
the disconnected symbols' bodies (query-time, deterministic, zero graph
mutation) for dynamic-dispatch forms — computed member calls, getattr,
reflection, typed message buses, runtime-keyed emits, Proxy — and announce
the exact site where the static path ends, with candidate runtime targets
when a dispatch key is statically visible. The honest alternative to
guessing edges: surface the boundary, don't fabricate the bridge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent-eval): ab-new-vs-baseline survives files added since the baseline ref
A single multi-file 'git checkout <ref> --' with one unknown pathspec checks
out nothing, so the baseline arm silently ran the NEW build. Check out
per-file and remove files that don't exist on the baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(playbook): boundary surfacing as the mechanism floor for non-gateable dispatch (#687)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(explore): render a direct synthesized hop between two named symbols (#687)
A 2-node chain populates pathIds but renders nothing (Flow needs >=3), and
the dynamic-links section skipped its edge as 'already in the main chain' —
so a custom EventBus emit→handler connection was invisible. Skip-as-in-chain
now applies only when a chain actually renders, and the boundary scan treats
short-chain endpoints as connected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds anonymous usage statistics (commands/tools used, languages indexed,
connecting agents) with a strict, auditable allowlist. Never code, paths,
file/symbol names, queries, or IPs.
- src/telemetry/: zero-dep client — consent resolution (DO_NOT_TRACK >
CODEGRAPH_TELEMETRY > stored choice > default-on), random machine UUID,
in-memory counters → capped JSONL buffer → completed-day rollups; sync
exit-append (survives process.exit) + opportunistic bounded sends; the
first-run notice gates the first SEND, never local buffering, so the
installer's consent toggle always precedes it. Off is off: no recording,
no socket, buffered data deleted.
- codegraph telemetry status|on|off; per-command counting via preAction hook.
- MCP: tool counting after the reply is on the wire (session + proxy
in-process fallback), agent attribution from initialize clientInfo,
unref'd daemon flush interval. Zero hot-path cost, zero stdout.
- Installer: visible default-on consent toggle (asked once, never re-asked),
install/index/uninstall lifecycle events.
- telemetry-worker/: public Cloudflare Worker behind telemetry.getcodegraph.com
— allowlist validation, IP stripping, per-machine rate limit, forwards to
PostHog as anonymous events. Ships nowhere with the npm package.
- TELEMETRY.md (field-by-field contract) + README section + design doc.
- 20 unit tests; suite-wide CODEGRAPH_TELEMETRY=0 guard so tests never
pollute real telemetry. Full suite: 1448 passing.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Task-tool subagents never see the MCP initialize instructions and hold
the MCP tools only as deferred names they rarely think to load — so
delegated work bypassed codegraph almost entirely (measured ~1 of 9
forced-delegation runs touched it; the rest did 30-50 grep/read calls).
Two additions close the gap:
- CLI: `codegraph explore` and `codegraph node` call the same ToolHandler
as the MCP tools and print identical output — the graph for any agent
with a shell (subagents, Gemini CLI, raw Codex, humans).
- Installer: each agent target (claude/codex/gemini/opencode) writes a
short marker-fenced CodeGraph section into its instructions file —
the one channel subagents DO receive — naming both surfaces. Upsert
self-heals the stale pre-#529 long block; uninstall strips it; re-runs
are byte-equal unchanged. (#529's duplication argument bounded the
size: four lines, commands only.)
A/B (excalidraw, sonnet/high, forced Explore-agent delegation): without
the block, subagent codegraph usage ~1/9 runs; with it, 4/4 — subagents
ToolSearch-load the MCP tools and run explore 5-7x, best runs with ZERO
Read/grep (80-95s vs 150-197s baseline). The block's mechanism: the
parent relays the note into the task prompt, making the deferred tool
names salient.
Contract tests updated to the new expectations (write + self-heal
replace the #529 strip-only behavior); README install/guidance sections
refreshed (they also still described the pre-#817/#818 tool surface).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
callees/impact/files/status stay fully functional (handlers, CLI, library
API untouched; CODEGRAPH_MCP_TOOLS re-enables any) but are no longer
LISTED by default. Evidence: codegraph_impact appears in zero recorded
eval runs ever; its blast-radius info already arrives inline on explore
(Blast radius section) and node (dependents note). callees is redundant
by construction (a symbol's body IS its callee list). files/status
"reduce to one grep" per the tiny-repo audit, and staleness banners
already inline pending-sync. callers stays: exhaustive call-site
enumeration (incl. callback registrations, per-definition sections) is
the one job explore/node don't replicate. Fewer tools = fewer mis-picks
+ ~300 schema tokens saved per session; presence itself steers.
server-instructions rewritten around the 4-tool surface ("what does X
call" → node body+trail; "what breaks" → callers + inline blast radius).
Tiny-repo gate unchanged (its trio ⊆ the default set); stale gate
comment corrected (context/trace are long gone — its "5 core tools" are
today's trio).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
An MCP session in a workspace with no .codegraph/ previously got the full
"lean on codegraph for everything" playbook plus all 8 tools, then every
call returned isError — and one or two early errors teach an agent to
abandon codegraph for the whole session (maintainer-observed). Now the
initialize response picks an instructions variant by index state (cheap
sync walk-up, #172 respond-fast contract holds) and tools/list serves an
EMPTY list when unindexed: absence is the one signal an agent can't
misread. Indexing is deliberately the user's call — the inactive note
tells the agent not to run init itself.
No-error policy in the tool handler: expected/recoverable conditions
(NotIndexedError — cross-project query to an unindexed path, default-
project detection miss) return SUCCESS-shaped guidance instead of
isError; security refusals (PathRefusalError) stay hard errors without
retry encouragement; genuine internal failures keep isError but add a
retry-once note so a transient blip doesn't convert to permanent
abandonment. Principle recorded in CLAUDE.md.
Also: codegraph_search kind:"type" (advertised by its own schema enum)
silently matched nothing — now maps to type_alias; codegraph_explore's
query param no longer tells agents to run codegraph_search first
(contradicted explore's call-FIRST design); server-instructions
§Limitations rewords the unindexed case to stay-out-for-the-session.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
.astro files were not indexed at all, leaving a typical Astro site mostly
invisible to search/impact/explore. New AstroExtractor (Svelte/Vue SFC
pattern): component node per file, TS frontmatter + <script> blocks
delegated to the TypeScript extractor, template {fn(...)} calls (incl. the
multiline `{posts.map((post) => (` opening line), PascalCase component-tag
references. New astroResolver: Astro global + astro:* virtual modules as
framework-provided, component resolution with the #764 ambiguity rule,
src/pages/ file-based routes ([param]→:param, [...rest]→*rest, _-prefixed
and *.config.* excluded). SFC languages now preload the TS/JS grammars
their extractors delegate to (a pure-SFC file set previously had none
loaded). Also fixes a pre-existing Svelte/Vue script-block off-by-one that
reported every script symbol one line low.
Validated per the playbook: stalux (the issue's repro) 54/54 .astro files
indexed, getIconNode found at its exact line, 14/14 routes, 93.0% fair
cross-file coverage; AstroPaper 27/27 components, 13/13 routes (underscore
dirs correctly excluded), explore connects page→Card→Datetime through the
jsx-render synthesizer; node/edge counts stable across re-syncs.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #813: the React resolver's blind components[0] fallback was
the demonstrated wrong-edge source, but the Svelte and Vue resolvers had
the same flaw in their own shape:
- svelte: resolveComponent fell back to components[0] across the whole
repo when no same-directory match existed — an arbitrary pick among
same-named components in a multi-app monorepo.
- vue: resolveComponent returned the FIRST basename-matching .vue file
found anywhere in the tree; its same-directory pass below was
unreachable dead code. apps/a/Button.vue vs apps/b/Button.vue was a
file-enumeration-order coin flip.
Both now follow the #764 rule: same-directory first, otherwise only an
UNAMBIGUOUS name resolves — ambiguity falls through to the name-matcher's
proximity scoring instead of guessing.
Safety: zero-delta A/B on the README's own framework benchmark repos
(sveltejs/realworld — the 100% Svelte coverage repo — and nuxt/movies,
93.5% Vue coverage) plus the excalidraw control: node counts identical,
zero calls or references edges changed. Single-app repos have unique
component names, so the rule only bites where the old behavior was
already a coin flip. Full suite 1398 passed.
Also verified the #813 per-definition tool grouping is language-agnostic
(probed Go same-named functions across packages — grouped identically to
the TS fixture).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A NestJS-style monorepo has one UserService/UserModule/UserRepository per
app; with no package concept for TS they share one global name scope and
agents visibly warned that CodeGraph was mixing unrelated classes.
Two distinct problems, two fixes:
1. TOOL AGGREGATION. callers/callees returned one merged list across every
same-named match, and impact merged all their blast radii into a single
overstated subgraph. Now: matches group into DISTINCT DEFINITIONS
(filePath + qualifiedName — same-file overloads still merge, that's the
overload feature) and render one file-labeled section per definition;
a new `file` argument (path or suffix, like codegraph_node's) narrows
to one definition, suppressing the stale aggregation note; a
non-matching `file` falls back to all definitions with a note.
server-instructions documents the behavior.
2. RESOLUTION WRONG EDGES. Auditing a real monorepo (amplication, 54k
nodes) found 1,036 cross-package `references` edges into duplicated
names. Root cause: the React framework resolver ran PascalCase
component resolution on refs from PLAIN .ts FILES (a GraphQL types
file's own `Account` type alias lost to an arbitrary same-named CLASS
in another package — the resolver's blind `components[0]` fallback at
confidence 0.8 outranked the name-matcher's proximity-correct 0.7).
Component resolution is now gated to JSX-capable refs (tsx/jsx) and
never guesses among multiple candidates without a positional signal
(same-dir / component-dir / unique). Cross-package wrong edges:
1,036 -> 40 (-96%; the remainder are genuine shared-model imports and
codegen template scaffolds), with the freed refs re-resolving to the
correct same-file/same-package targets. excalidraw (a real React repo)
is a zero-delta control — legitimate component refs all carry
same-dir/component-dir signals.
Graph-level separation was verified correct on a fixture before any
changes (import + proximity resolution keeps apps apart) — the conflation
was tool-level plus the react-resolver edge class.
Tests: 6-test e2e suite (grouped callers/callees, per-definition impact
radii, file narrowing, fallback note, cross-app edge isolation) + react
resolver unit tests updated to production reality (tsx refs resolve,
plain-ts refs decline). Full suite 1398 passed. EXTRACTION_VERSION
23 -> 24 (re-index to drop the wrong cross-package edges).
Closes#764
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`KtHandlers::handle` registered from another file produced no edge: the
extraction gate required the scope to be a same-file type or an IMPORTED
name, but Java/Kotlin same-package references and Kotlin companion members
need no import at all, so the gate could never see them. (The "companion
members extract unqualified" limit recorded during Arc A was a probe
artifact: a SINGLE-LINE `class X { companion object { … } }` is an
upstream tree-sitter-kotlin misparse (ERROR node); real multi-line
companions extract transparently as qualified methods of the class.)
Qualified `Type::member` candidates now skip the name gate the same way
`this.<member>` ones do: the explicit-ref syntax is self-selecting, and
resolution stays scope-suffix-anchored + unique-or-drop, so a
`Decoy::handle` can never match a `KtHandlers::handle` ref (tested).
A/B vs main: rxjava +4 (same-package `Maybe::just` / `Single::just`
method refs), fmt +3 (gtest `&Test::DeleteSelf_` /
`&TestSuite::RunSetUpTestSuite` cross-file member pointers), okio 0-delta,
redis byte-identical — every new edge verified genuine, zero calls edges
touched, node counts identical.
Full suite 1392 passed. EXTRACTION_VERSION 22 → 23 (re-index to benefit).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last two deferred callback-registration shapes from #756, each scoped
to positions where the reference is trustworthy:
PHP — a string is a callable ONLY in a known callable position:
- string args of core HOFs (usort, array_map, array_filter,
call_user_func*, preg_replace_callback, spl_autoload_register,
set_error_handler, … — PHP_CALLABLE_HOFS): ungated (PHP globals are
referenced cross-file without imports) + resolution unique-or-drop,
function-kind only ('Cls::m' strings resolve qualified)
- array callables anywhere in call args: [$this, 'method'] routes through
the class-scoped this. resolver (parents included); [Foo::class,
'method'] resolves qualified
- strings to arbitrary functions: deliberately nothing
Ruby — hook-DSL symbols name a method of the enclosing class:
(skip_)?(before|after|around)_* / validate / set_callback /
helper_method / rescue_from(with:) symbols → class-scoped this.<sym>,
riding the supertype pass so `before_action :authenticate` in a
controller resolves to ApplicationController's method. `validates`
(plural) excluded — its symbols name ATTRIBUTES. Class-body-level hooks
attribute to the CLASS node (the scoped resolvers now accept class-like
from-nodes).
Also hardened while validating: the this.X supertype pass is now
NODE-anchored — file-anchored class node → implements/extends edge targets
→ contains-anchored member lookup — replacing the name-keyed
getSupertypes walk, which unioned every same-named class's parents (rails
has a dozen `Engine`s) and produced a cross-class wrong edge.
A/B vs main: WordPress +556 (14/14 sampled genuine — [$this,'m'] wiring,
array_map('absint',…), sodium polyfill call_user_func_array dispatch);
rails/rails +385 after the node-anchored fix (16/16 sampled genuine, incl.
inherited hooks across real extends edges); controls byte-stable
(excalidraw 0-delta, redis identical, typeorm keeps its +4 inherited
getters). The only calls-edge deltas anywhere are pre-existing
minified-bundle resolution jitter (wp-tinymce.js single-letter symbols).
Full suite 1391 passed. EXTRACTION_VERSION 21 → 22 (re-index to benefit).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three callback-registration shapes deferred from #756/#808, one arc:
1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>`
registration whose member isn't on the enclosing class defers to a second
pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs,
runs after implements/extends edges persist, same lifecycle as the #750
conformance pass) and resolves up the supertype chain, depth-capped BFS,
validated targets only. `bus.on("submit", this.handleSubmit)` in a
subclass links to FormBase::handleSubmit; same-named methods on unrelated
classes never match. this.-prefixed candidates skip the extraction name
gate (an inherited member can't be in definedHere).
2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` /
`OtherClass::handle` emit QUALIFIED names resolved by the scoped
suffix-matcher — cross-file capable, gated on the scope name being a
same-file type or an imported name (dotted JVM imports now contribute
their last segment). `this::m` and `super::m` route through the
class-scoped resolver (super rides the supertype pass). References
through a VARIABLE (`subscriber::onNext`) deliberately produce nothing —
receiver type is unknowable; RxJava's baseline bare capture was resolving
these to same-named same-file methods (a test method "registering" an
anonymous class's onNext) — the rework drops 18 such wrong edges and
keeps the 7 genuine Type::method refs RxJava's main tree actually has.
3. SWIFT enclosing-type scoping (implicit self): bare callback names match
methods only of the from-symbol's own type (extension/nested scopes
reconciled by suffix), and top-level code never matches methods.
Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier`
resolving to same-named methods on unrelated protocols), all verified;
the same-class param collision (`task`) remains and is documented.
New ResolutionContext.getNodeById lets matchers derive the from-symbol's
class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw
stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges
changed on any of 7 A/B repos; nodes identical everywhere. Kotlin
companion-object members extract unqualified (pre-existing) so
`Type::companionFn` stays silent rather than guessing — documented.
Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).
Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).
With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.
A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).
EXTRACTION_VERSION 19 → 20 (re-index to benefit).
Closes#808
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A function name used as a VALUE — passed as an argument
(signal(SIGINT, handler), qsort(..., compare)), assigned to a function
pointer or field (ops->recv_cb = my_cb, OnClick := Handler), or placed in
a struct initializer / handler table ({ .recv_cb = my_cb },
{ "get", getCommand }) — produced no edge in ANY of the 19 tree-sitter
languages, so registered callbacks looked dead and their registration
sites were invisible to callers/impact.
This adds table-driven function-as-value capture across all 19 languages
(plus the wrapper forms: &fn, &Cls::method, Java Class::m, Kotlin ::f,
Swift #selector, ObjC @selector, Ruby method(:sym), Scala eta, Pascal
@Handler), gated at extraction (same-file definitions + imported
bindings; C-family file-scope initializers are constant-expression
contexts and skip the gate, which is how redis-style cross-file command
tables resolve), and resolved by a dedicated strategy: function/method
targets only, same-file first, unique-or-drop cross-file, no fuzzy
fallback ever. Edges persist as kind 'references' with metadata.fnRef,
so getCallers/getImpactRadius surface them with zero graph-layer
changes; MCP callers/callees label them "via callback registration".
Precision rules bought by real-repo false positives (full A/B record in
docs/design/function-ref-capture.md): C++ is &-explicit outside
file-scope tables (fmt's begin/out/size collisions; out-of-line member
defs are function-kind); TS/JS/Python bare ids resolve to functions only
(TS class fields extract as method-kind — pre-existing quirk); Swift
refuses same-file method overload-families; param-forward shapes
(this.x = x, value: value) and destructuring are skipped; minified
bundles (*.min.js) produce no candidates.
Validated on 17 public OSS repos (redis, excalidraw, gin, bytes, okhttp,
okio, Alamofire, flask, sinatra, Newtonsoft.Json, scopt, provider,
busted, Fusion, AFNetworking, PascalCoin, fmt): node counts identical,
zero calls edges lost or gained, references strictly additive
(+3,200 registration edges total), precision spot-checked by reading
sampled source lines (redis 30/30, flask 8/8). Deliberately NOT covered:
indirect-dispatch resolution (o->cb(x) → impl) — that needs data-flow
through struct fields, and a wrong edge is worse than none.
EXTRACTION_VERSION 18 → 19 (re-index to benefit).
Closes#756
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(extraction): capture docstrings for export/const/decorator-wrapped symbols (#780)
getPrecedingDocstring walked previousNamedSibling from the EMITTED
declaration node, so it only found a leading comment when the comment was
a direct sibling of that node. For a declaration nested under a wrapper —
`export class X` / `export const f = () => {}` (export_statement /
lexical_declaration), a plain const arrow (variable_declarator), or a
decorated Python def/class (decorated_definition) — the comment is a
sibling of the WRAPPER, so the inner node had no preceding comment and
the docstring was stored as NULL.
Climb out through the wrapper node(s) before scanning for the comment.
Each wrapper holds exactly one declaration, so this can't mis-attribute a
comment to a sibling (verified: an uncommented method does NOT inherit its
class's comment). Also strip leading `#` from Python/Ruby/shell line
comments, which the cleanup chain missed (Python docstrings used to keep
their `#`).
Query/extraction-layer change to a parse helper; re-index to pick up
docstrings on already-indexed files. Verified on the reporter's JS/TS and
Python repros (8/8 now captured) plus over-walk controls; +3 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(extraction): clean comment markers across all supported languages (#780)
Validating docstring capture across every README language surfaced that
the marker cleanup only knew C-style `//` and `/* */`, plus the `#` added
earlier this branch. Doc comments in other styles were captured but left
their markers in the stored text:
- Rust/Swift/Kotlin doc lines `///` and `//!` -> leading `/` / `!` leaked
- Lua/Luau `--` and `--[[ ]]` -> not stripped
- Pascal `{ }` and `(* *)` -> not stripped
Extract the cleanup into cleanCommentMarkers() and handle every style.
Paired block delimiters are stripped only when the comment OPENS with one,
so a line comment that happens to end with `}` / `*)` / `]]` is never
truncated; per-line markers stay anchored at line start.
Validated end-to-end (extract -> index -> codegraph_node output) across
all 19 tree-sitter code languages plus Svelte/Vue `<script>` blocks: every
one now stores and returns a clean docstring. +1 cross-language test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A stdio MCP server's lifeline is stdin: when the host/client goes away,
stdin should end and the server should exit. The server paths listened
for stdin 'end'/'close' but NOT 'error'.
That gap bites with a socket-backed stdin — the shape VS Code / Claude
Code use (a socketpair, not a pipe). On client death the socket can
surface as an 'error' (ECONNRESET/hangup) instead of a clean 'close'.
Unhandled, it escalated to the process-wide uncaughtException handler,
which logs and keeps running — so the server orphaned instead of
exiting. On Linux a POLLHUP socket fd left registered in epoll then
wakes the event loop continuously, pinning a core at 100% CPU; once the
main thread spins, the setInterval PPID watchdog can't even fire, so the
orphan runs forever (the report's 28+ minutes).
Add treatStdinFailureAsShutdown(): listen for 'error' as well as
'end'/'close', and DESTROY the stdin stream on any terminal event so the
fd leaves epoll and can't churn, then run the path's shutdown. Wired into
the live paths — startDirect, the local-handshake proxy, and
StdioTransport — plus the legacy pipe proxy. Fires once (re-entry guard).
Note: this is hardening for a class of failure that matches every piece
of the report's evidence (socket stdin, userspace main-thread spin, high
involuntary context switches, watchdog never firing), but the exact 100%
CPU spin could not be reproduced in Docker (Linux) across /dev/null EOF,
socket peer-death (RST/FIN), the reporter's 0.9.7 bundle, and the npx
chain — all exited cleanly — so the trigger is environment-specific.
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>
Versions <= 0.9.9 wrote an explicit-allowlist .codegraph/.gitignore
(*.db, cache/, .dirty, ...) that never listed daemon.pid or the socket,
so the daemon's runtime pidfile got committed. The wildcard rewrite in
#654/#492/#484 fixed new inits, but the file is only written when
absent, so existing installs kept their stale file forever — the fix
never reached the people hitting it.
Make the gitignore self-heal: ensureGitignore() writes the file if
absent and upgrades a stale CodeGraph-generated default in place,
leaving a user-authored file untouched. A "stale default" is one that
carries our `# CodeGraph data files` header but predates the wildcard
ignore (no bare `*` line) — a header match heals every historical
variant (v0.7.x..0.9.9, all verified to share it) and is idempotent.
validateDirectory() runs on every open()/openSync(), so existing repos
heal on the next codegraph command after upgrading. The duplicated
template (previously inlined in two formats) is consolidated into one
GITIGNORE_CONTENT constant.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Pascal/Delphi procedure or function defined ONLY in the implementation section
(no interface declaration, not a class method) had no node of its own, so
extractPascalDefProc's caller lookup fell through to the nodeStack top — the file
node. Every call in such a routine's body was lumped under the unit: callers
returned the file, and impact couldn't attribute the call to the routine. (Methods
were fine — they get a node from their class declaration.)
Fix: when extractPascalDefProc finds no existing node for a FREE routine (a name
with no `.`), create a function node for it and attribute the body's calls to it.
Interface-declared free routines already have a node (found via the methodIndex),
so there's no duplicate; methods keep their existing class-declaration node.
PascalCoin A/B: +511 / -145 — the +511 are calls now correctly attributed to their
actual routine (`allocate_new_datablock -> TDisposables::GetMem`), replacing -145
file-level aggregates; +248 new function nodes for the implementation-only
routines. New synthetic test asserts a free routine's call attributes to it
alongside a method caller. EXTRACTION_VERSION 17->18. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pascal/Delphi lets a no-arg method or procedure drop its parens, so the call
parses as a bare `exprDot` (not an `exprCall`) and was never recorded as a call —
callers/impact/trace missed all of them (e.g. `Obj.Free`, `List.Clear`, the
paren-less factory chain `TFoo.GetInstance.DoIt`).
extractPascalParenlessCall handles these, wired into visitPascalBlock scoped to
STATEMENT position only: a bare `Obj.Field;` statement is a no-op, so a
statement-level dot expression is a call — but a dot in assignment LHS/RHS or a
condition is left alone, since there it's genuinely ambiguous with a
field/property access. The chained paren-less form reuses the #750 chain encoding
(gated on the Delphi `TFoo`/`IFoo` type convention) and resolves the same way.
PascalCoin A/B: +1131 / -1 — purely additive, and all 1131 new edges resolve to
METHOD nodes (zero field/property false positives, confirming the statement-level
gate). 3 new synthetic tests (paren-less call, paren-less chained factory, and the
property-write/read non-extraction guard). EXTRACTION_VERSION 16->17. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the #645/#608 chained-receiver mechanism to Pascal/Delphi — which I'd
previously mis-scoped as blocked. The paren'd chained form extracts fine; it just
hit the chained-call gap like the others (with a decoy, `TFoo.GetInstance().DoIt()`
mis-resolved to a same-named method on an unrelated class).
- pascal.ts: getReturnType reads the method's `typeref` (a `function GetInstance:
TBar` returns TBar; an interface return `IFoo` is captured too).
- tree-sitter.ts: extractPascalCall now re-encodes a chained call `TFoo.GetInstance().DoIt`
(the exprDot's receiver is an exprCall) instead of collapsing it to bare `DoIt`.
Gated on the Delphi type-naming convention (`TFoo`/`IFoo`) so a capitalized
VARIABLE chain (Pascal capitalizes locals too — `Curve.X().Y()`, `Self.X().Y()`)
stays bare and keeps its existing bare-name resolution.
- name-matcher.ts: `pascal` joins the dotted-chain gate + CHAIN_LANGUAGES +
CONSTRUCTS_VIA_BARE_CALL (a `TFoo(x)` typecast yields a TFoo). When the factory's
return type wasn't captured (a `constructor Create` has no `: TBar` but returns
its class), resolve the method on the factory class itself. resolveMethodOnType
validates, so a wrong inference yields no edge.
Validation: 4 synthetic tests (factory+decoy, constructor chain, typecast chain,
absent-method safety). Real-repo A/B on PascalCoin (772 files): +19 / -18 — 15 of
the -18 are correct class→interface retargets (`GetInstance(): IAsn1OctetString`
resolves `.GetOctets` on the declared interface, not baseline's concrete-class
guess); 3 are negligible drops (0.02%). EXTRACTION_VERSION 15->16. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the #645/#608 chained-receiver mechanism to Objective-C. A message send
whose receiver is itself a message send — `[[Foo create] doIt]` — used to drop
the receiver, so `doIt` name-matched a same-named method on an unrelated class
(commonly a test helper's `init` or an Apple-SDK method).
- objc.ts: getReturnType reads the method's `method_type`, SKIPPING nullability /
ARC qualifiers (`nonnull instancetype` must yield instancetype, not `nonnull`).
- tree-sitter.ts: the message_expression branch now re-encodes a chained send
`[[Foo create] doIt]` as `Foo.create().doIt` when the inner receiver is a
capitalized class and the outer selector is unary.
- name-matcher.ts: `objc` joins the dotted-chain gate + CHAIN_LANGUAGES. A
class-message factory returns an instance of the RECEIVER class by convention
(`instancetype`), so when the factory's own return type isn't recoverable
(`alloc`/`new`/`shared…` return instancetype, or aren't user nodes), the
receiver's type is the class itself — this resolves the ubiquitous
`[[X alloc] init]` and singleton chains. resolveMethodOnType validates against
the class and its supertypes, so a wrong inference yields no edge.
Validation: 4 synthetic tests (factory+decoy, superclass conformance, absent-method
safety, the nonnull-instancetype singleton). Real-repo A/B on SDWebImage (208 files):
+35 / -75 — all corrections (the -75 are wrong `init` mis-matches to a test helper /
wrong class, retargeted to the right class's init in the +35, plus 2 Apple-SDK chains
on unindexed classes). db stable, no node explosion. EXTRACTION_VERSION 14->15.
Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the #645/#608 chained-receiver mechanism to Dart, plus makes Dart factory
and named constructors first-class so their chains can resolve at all. A call
whose receiver is itself a call — `Foo.create().bar()` (static factory or
factory/named constructor) — used to drop the receiver to a bare `bar`, which
name-matched a same-named method on an unrelated type (commonly a stdlib
`Option`/`Iterator` `.map`/`.where` mis-tied to the project's own class).
- dart.ts: extractBareCall now re-encodes `Foo.create().bar` when the chain
starts with a capitalized type; getReturnType captures the return type (generic
`List<Foo>` → `List`); factory (`factory Foo.create()`) and named (`Foo._()`)
constructors are indexed as `Foo::create` / `Foo::_` with return type = the
class (via resolveName + getReturnType + constructor_signature in methodTypes).
- The UNNAMED ctor `Foo()` is deliberately NOT extracted (isMisparsedFunction),
so plain construction stays an `instantiates` edge to the class rather than a
call to a phantom `Foo::Foo` method.
- dartCtorInfo validates a "constructor" against the enclosing class name, so a
method tree-sitter MISPARSES as a constructor — `@override (A, B) m()`, where
the annotation swallows the record return type and `m()` looks like a one-id
constructor_signature — is still extracted as the method it is (regression
found on localsend; covered by a new test).
- name-matcher.ts / index.ts: `dart` joins the dotted-chain gate,
CONSTRUCTS_VIA_BARE_CALL (case construction), and CHAIN_LANGUAGES (conformance
for superclass/mixin methods). resolveMethodOnType validates, so a wrong
inference yields no edge.
Validation: 7 synthetic tests (static factory, factory/named ctor, construction,
conformance, absent-method safety, the misparse regression, instantiation-not-
hijacked). Real-repo A/B on localsend (368 Dart files): hand-written +17/-10 — all
corrections (the -10 = 7 wrong stdlib/extension misattributions removed + 3 ctor
source-renames), plus additive factory/named-ctor call resolution. Instantiation
preserved; no node explosion. EXTRACTION_VERSION 13->14. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call
whose receiver is itself a call — `Foo.create().bar()` (companion factory),
`Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the
receiver to a bare `bar`, which name-matched a same-named method on an unrelated
type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/
`.foreach` mis-attributed onto the project's own same-named class.
- scala.ts: `getReturnType` reads the `return_type` field — generic `List[Foo]`
→ container `List`, qualified `pkg.Foo` → `Foo`, `this.type` left undefined.
- tree-sitter.ts: re-encode `Foo.create().bar` when the inner call's receiver chain
starts with a capital (companion factory / case-class apply); instance chains
(`list.map().filter()`) stay bare.
- name-matcher.ts: `scala` joins the dotted-chain gate + CONSTRUCTS_VIA_BARE_CALL
(case-class `apply` constructs the class); resolveMethodOnType validates, so a
non-conventional `apply` returning another type yields no edge, not a wrong one.
- index.ts: `scala` joins CHAIN_LANGUAGES so trait-inherited methods resolve via
the conformance second pass.
Validation: 4 synthetic tests (factory+decoy, case-class apply, trait conformance,
absent-method safety). Real-repo A/B on gatling (750 Scala files): +14 / -59 unique
edges — all corrections. The +14 are retargets (e.g. `HttpProtocolBuilder(cfg).baseUrl`
now resolves to HttpProtocolBuilder::baseUrl, not the same-named private BaseUrlSupport
helper); the -59 are wrong edges removed (stdlib Option/Iterator monad calls
mis-tied to the project's Validation::*, self-loops, decoy collisions) — zero genuine
factory chains dropped (verified: gatling has no real Validation.success().map() chains).
db stable at 40 MB. EXTRACTION_VERSION 12→13. Full suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(go): resolve chained factory-function calls New().Method() (#750)
A Go call through a chained factory function — `New().Method()`,
`With(cfg).Build()` — dropped the receiver to a bare method name, which then
attached to a same-named method on an unrelated type (a wrong edge) or didn't
resolve. Ports the #645/#608 mechanism for Go's bare-factory receivers:
- Part 1: capture Go return types; a pointer `*Foo` -> `Foo`, a multi-return
`(*Foo, error)` -> its first result, qualified `pkg.Foo` -> `Foo`.
- Part 2: encode a bare-factory chain (`New().Method`), gated to an `identifier`
receiver so instance chains (`obj.Method().Other()`) keep bare-name.
- Part 3: matchDottedCallChain bare-inner Go branch looks up the FUNCTION's
return type, then resolves+validates the method on it. Wired into the
conformance pass so a method promoted from an embedded struct (`type Widget
struct{ Base }` -> the existing `extends` edge) resolves. FALLBACK: when the
inner isn't a resolvable function (a package-level VARIABLE holding a function
value, e.g. gin's `engine()`), fall back to bare-name so the edge isn't dropped.
Validated: synthetic decoy + args + multi-return + embedded-conformance + absent
safety tests (4/4); full suite green. Real-repo A/B on gin (99 .go): pre-fallback
-40 = 25 wrong self-loops removed (good) + 15 correct `Engine::ServeHTTP` dropped
(gin's ginS variable-factory `engine()`); the fallback recovers the 15. gin A/B
re-confirm with the fallback is PENDING (local index flakiness, not a code issue).
EXTRACTION_VERSION 11 -> 12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(go): stop the chained-call fallback from looping the batched resolver
The Go variable-inner fallback (for chains like `engine().ServeHTTP()` whose
inner is a package-level var, not a factory function) resolved the method via
a synthetic bare-name ref and propagated THAT ref as `.original`. Its
`referenceName` was the bare `ServeHTTP`, not the stored `engine().ServeHTTP`,
so `resolveAndPersistBatched`'s keyed `deleteSpecificResolvedReferences` no-oped,
the offset-0 batch never drained, and the loop re-resolved + re-inserted the
same rows forever — a runaway that grew a 99-file repo (gin) to 5,050,206 edges
/ 1.4 GB before filling the disk.
- name-matcher.ts: tie the bare-name match back to the original `ref` so the
batch-cleanup delete matches the stored row and the loop drains.
- index.ts: add a non-progress guard to resolveAndPersistBatched — if the
unresolved_refs table doesn't shrink after a batch, stop instead of growing
the graph without bound (defense-in-depth for any future keyed-delete mismatch).
- resolution.test.ts: regression test for the variable-inner chain — asserts the
fallback edge resolves AND the edge count stays bounded (no explosion).
gin A/B (post-fix): db 5.8 MB / 3,699 calls edges; net-zero unique-edge diff vs
main (the fallback recovers the dropped edges, adds no wrong ones). Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Rust call through a chained associated function — `Foo::new().bar()`,
`Foo::with(cfg).build()` — dropped the receiver to a bare method name, which
then attached to a same-named method on an unrelated type (a wrong edge) or
didn't resolve. Ports the #645/#608 mechanism for Rust's `::` receivers:
- Part 1: capture Rust return types; `-> Self` yields the `self` marker (resolved
to the impl's own type, like PHP), references/generics are unwrapped/reduced.
- Part 2: encode an associated-function chain (`Foo::new().bar`), gated to a
scoped_identifier receiver so instance chains (`x.foo().bar()`) keep bare-name.
- Part 3: resolve via matchScopedCallChain (PHP's `::` resolver, generalized),
validated by resolveMethodOnType. Wire Rust into the conformance second pass
(matchScopedCallChain variant) so a chained method provided by a trait the type
implements (`impl Trait for Type` → existing implements edges) resolves too.
Validated: synthetic decoy + args + Self + trait-default-conformance + absent
safety tests; full suite green (lone failure is the known-flaky #662 daemon test,
passes in isolation). Real-repo A/B vs main: clap (329 .rs) a net precision win —
**+937 added (96% correct builder methods), 622 wrong->right retargets**
(`Command::new().arg()` was mis-resolving to `ArgGroup::arg`, now `Command::arg`),
+162 net unique edges; the pure-drops are largely wrong bare-name edges the fix
correctly stops emitting. tokio-rs/bytes 0/0 (no regression). Known limit: the
single-hop mechanism re-encodes only the first hop of a chain (deeper hops keep
bare-name) — clap's unusually deep builder chains are partly covered.
EXTRACTION_VERSION 10 -> 11.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes Swift in the #750 chained-call series (after Java #751, Kotlin #752,
C# #753, conformance #754). Two parts:
1. Swift chained-call resolution (the #645/#608 mechanism): capture Swift return
types (positional, member types -> last segment), encode capitalized-receiver
chains `Foo.make().draw()` / `Foo(args).draw()`, resolve+validate via the
shared matchDottedCallChain (+ constructor branch). Fixes the decoy wrong-edge
bug where a chained method dropped to a bare name and attached to a same-named
method on an unrelated class.
2. Nested-type extension naming fix: `extension KF.Builder: KFOptionSetter` parsed
as a class_declaration named `KF.Builder` (dot) — inconsistent with the type's
own declaration `KF::Builder` (name `Builder`) — so the extension's conformances
and members were invisible to a chained call on the type. A Swift resolveName
now names a nested-type extension by its last segment (`Builder`), so its
`implements`/`extends` edges and methods are found by the supertype walk
(conformance #754) and the simple-name method match.
Validated: synthetic decoy + args + constructor + absent-method tests; full suite
green; nested-extension repro (`KF.url().onSuccess()` resolves via conformance to
the protocol method). Real-repo A/B vs main (conformance) — Alamofire and
Kingfisher both **0 added / 0 removed, node count unchanged**: NEUTRAL and SAFE.
The prior -168 Kingfisher regression (from the naming inconsistency) is eliminated;
Swift's unique-named fluent methods already resolved by bare name, so the chain
path lands the same edges — the value here is decoy-collision correctness, the
nested-extension naming fix, and consistency with the other four languages.
EXTRACTION_VERSION 9 -> 10.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(resolution): conformance-aware chained-method resolution (#750)
A chained static-factory/fluent call whose method lives on a SUPERTYPE the
receiver conforms to — a protocol-extension method (Swift), an interface default
method, or an inherited superclass method — now resolves. resolveMethodOnType
falls back to walking the return type's implements/extends edges (via the new
context.getSupertypes) when the method isn't a direct member. Because those edges
don't exist during the single-pass resolution, a second pass
(resolveChainedCallsViaConformance) re-resolves the deferred chained refs after
edges are built. Still validated, so a wrong inference yields no edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): conformance-aware chained-method resolution (#750)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A C# method called through a static factory or fluent chain —
`Foo.Create().Bar()`, `JObject.Parse(s).Property(...)`,
`Instant.FromUtc(...).InZone(zone)` — lost the receiver's type, so the chained
method didn't resolve and the call was invisible to callers/impact/trace. Ports
the #645/#608 mechanism to C# (additive, like Java #751):
- Part 1: capture C# return types in the extractor, reading the `returns` field
(`static Foo Create()` -> `Foo`); predefined/array/generic/nullable/namespaced
types are normalized or skipped.
- Part 2: encode a chained `member_access_expression` receiver
(`Foo.Create(args).Bar()`) as `inner().Bar` with normalized empty parens, so
factory calls that take arguments still split. Non-chained member calls keep
their existing `recv.Method` text.
- Part 3: resolve via the shared matchDottedCallChain (now Java/Kotlin/C#),
validated by resolveMethodOnType so a wrong inference yields NO edge.
Known limitation (safe): C# extension-method chains don't resolve, since the
method lives on the extension class, not the receiver's type — no edge, never a
wrong one.
Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on Newtonsoft.Json (945 .cs: +3, 0 lost) and nodatime (488 .cs:
+73, 0 lost) — node count identical (no explosion), 0 edges lost, precision
spot-checked verbatim (Instant.FromUtc().InZone(), Offset.FromHoursAndMinutes().Plus(),
OffsetDateTimePattern.CreateWithInvariantCulture().WithTwoDigitYearMax()).
EXTRACTION_VERSION 7 -> 8.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Kotlin method called through a companion-object factory, fluent chain, or
constructor — `Foo.getInstance().bar()`, `Config.create(opts).build()`,
`STMTransaction(f).commit()` — dropped the receiver to a BARE method name, which
then name-matched a same-named method on an unrelated class (a wrong edge) or
failed to resolve. Ports the #645/#608 mechanism to Kotlin:
- Part 1: capture Kotlin return types in the extractor. tree-sitter-kotlin
exposes no field names, so the return type is read positionally (the type node
after function_value_parameters); inferred/Unit/Nothing returns yield none.
- Part 2: encode a CLASS/companion-factory call-receiver chain as `inner().method`.
Gated to a capitalized receiver (`Foo.getInstance()` / `Foo(args)`) so instance
chains (`list.filter{}.map{}`) keep their bare-name behavior — re-encoding those
would only drop the edge, regressing recall in fluent codebases.
- Part 3: generalize matchJavaCallChain -> matchDottedCallChain (shared by the JVM
dot-notation languages); resolve the method on the factory's return type, or on
the constructed class for a Kotlin `Foo(args).method()` receiver. Validated via
resolveMethodOnType, so a wrong inference yields NO edge.
Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on arrow-kt/arrow (734 .kt) — node count identical (no explosion),
+49 validated-correct chained edges, and the removed edges are wrong bare-name
guesses the fix correctly stops emitting (419/438 from test/doc files; the 18
from product code are stdlib `.apply{}`, self-loops, and bare-name mismatches) —
a net precision improvement, ~0 correct product edges lost. Java path unchanged
(constructor branch is Kotlin-gated). EXTRACTION_VERSION 6 -> 7.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Java method called through a static factory or fluent chain — `Foo.getInstance().bar()`,
`Config.create(opts).build()` — lost the receiver's type, so the chained method either
didn't resolve at all or (when a same-named method existed on an unrelated class) attached
to whichever class was indexed first. Ports the #645 (C++) / #608 (PHP) 3-part mechanism:
- Part 1: capture Java return types in the extractor (skip void/primitives/arrays,
unwrap generics, strip package qualifier).
- Part 2: encode a chained-call receiver as `inner().method` with normalized empty
parens, so factory calls that take arguments still split.
- Part 3: matchJavaCallChain resolves the chained method on the factory's return type,
validated via resolveMethodOnType so a wrong inference yields NO edge (never a wrong one).
Validated: synthetic decoy + absent-method safety tests; real-repo A/B on google/guava
(3,227 files) — node count identical (no explosion), 0 edges lost, +1,507 unique chained
edges recovered, precision spot-checked verbatim (Splitter.on().split(),
CacheBuilder.newBuilder().recordStats(), GraphBuilder.directed().build(), nested
MultimapBuilder.linkedHashKeys().arrayListValues()). EXTRACTION_VERSION 5 -> 6.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A method called through a PHP fluent static factory — `ApiClient::for($c)->createOrder()`,
the canonical Laravel per-credential/per-tenant client idiom — produced no
`calls` edge: the receiver of `->createOrder` is the `Cls::for(...)` static
call, whose result type was never recovered, so the edge was dropped and
`codegraph_callers` returned nothing.
Same shape as the C++ singleton/factory fix (#645), reusing its return_type
column + the chained-call mechanism:
- Capture PHP return types (getReturnType): `: self` / `: static` / `$this`
stored as the `self` marker, a concrete `: Type` as its short name,
primitives/unions dropped.
- Encode the chained scoped-call receiver as `Cls::for().method` so the
resolver can split it (PHP-gated, in extractCall).
- New matchPhpCallChain: look up the factory's return type (`self` → the
factory's own class; concrete → that class), then resolve AND validate the
method on it — a wrong inference yields no edge, never a wrong one.
EXTRACTION_VERSION 4->5 (re-index to populate PHP return types + chained edges).
Validated on koel (1383 PHP files): node count identical (no explosion),
0 edges lost, +80 chained-call edges recovered; synthetic tests cover the
self-factory, concrete-return, namespace, decoy, and absent-method cases.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-word path fix (#745) brought the backend to parity but not above:
the project name still gave the lexically-matching stack a residual dir
match + an FTS class-name match, so a backend query that included the
project name still ranked the frontend at/above the backend.
Derive the project name from go.mod module / package.json name / repo dir,
and treat a query word matching it as non-discriminative: drop it from path
relevance and from codegraph_explore's PascalCase type-disambiguation bias
(reporter's suggestions #1/#2) — unless it's the only query word, so a bare
project-name search still scores.
Narrow by construction: the down-weighting fires ONLY when a query word
matches the derived project name (≥5 chars), so every query that doesn't
name the project is byte-identical. On the reporter's repro the backend
controllers now top a backend question that includes the project name;
queries without it, bare project-name queries, and normal symbol queries
are unchanged. Query-time only (no re-index).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-word PascalCase query token — typically a project name a user
includes (`SuperBizAgent backend routes`) — splits into sub-tokens
(superbizagent / super / biz / agent) that ALL match the same path segment,
so path relevance summed +5 four times for one concept. In a mixed-stack
repo that ~doubled every score of the lexically-matching stack's file,
burying the stack the query was about.
Score path relevance per original query WORD instead: a word matches a path
level if any of its sub-tokens do, and counts once — while still splitting
the word (via extractSearchTerms on the original case) so it matches across
naming conventions (`getUserName` → `get_user_name`). Distinct words each
still contribute.
Partial fix: this removes the dominant path over-counting (backend rises
from absent-in-top-6 to parity on the reporter's repro). The residual lexical
edge from the project name in the FTS class-name match + dir match is a deeper
down-weighting change, tracked separately. No re-index needed (query-time).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A function called only from an anonymous func_literal at package level — a
cobra `RunE: func(){…}` handler, a goroutine literal, a callback closure
stored in a `var` — had its call leak to the FILE node, because the Go
var-initializer walk ran with an empty scope. So `callers`/`impact` showed
the function with a file (or no meaningful) caller, unlike JS/TS where an
arrow-in-const becomes a named node whose calls attribute correctly.
Scope the Go top-level var/const initializer walk to the declared symbol, so
a call nested in any func_literal initializer (struct field, slice/map,
nested closure) attributes to the enclosing var. EXTRACTION_VERSION 3->4
(re-index to pick up the corrected attribution).
Validated on cli/cli (858 Go files): node/edge counts identical, file-level
dependents byte-identical (no regression), and 62 top-level-closure calls
correctly moved from file-attributed to var-attributed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A .gitignore transparently encrypted in place by corporate DLP / endpoint
software (UTF-16 header + ciphertext), or one containing a pattern the
`ignore` library can't compile to a regex (`\[` -> "Unterminated character
class"), crashed the entire sync/index. The throw is LAZY — it surfaces at
match time (`ig.ignores()`), not `.add()` — so the existing add-time
try/catch never caught it, and the error never named the offending file.
Read .gitignore defensively: skip a file that isn't valid UTF-8 text whole
(NUL byte or fatal UTF-8 decode), drop only the individual uncompilable
patterns from a text one (probe-compile, then per-line fallback), and warn
with the file path. Indexing continues either way. The watcher inherits the
fix via buildDefaultIgnore.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PHP's importTypes only captured namespace_use_declaration, so
include/require(_once) — the dependency mechanism in procedural and
script-style PHP — never produced edges. callers, impact, and trace
missed the entire file-include graph; only namespace `use` became a
dependency edge.
Capture the four include/require expression types and emit file→file
imports edges, reusing the path-based resolution that C/C++ #include
already goes through. Only static string-literal paths are resolved
(relative to the including file); dynamic forms (include $var,
require __DIR__ . '/x', interpolated strings) are skipped.
Include PATHS are distinguished from namespace `use` symbols by shape: a
path contains '/' or '.', which PHP identifiers and FQNs never do. A
path-shaped include that doesn't resolve to a known project file is left
unresolved and does NOT fall back to the symbol name-matcher, which would
otherwise mis-connect "inc/db.php" to an unrelated db.php elsewhere — a
wrong edge is worse than a missing one.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
A C++ method call whose receiver is another call's result — `Foo::instance().bar()`,
`WidgetFactory::create().draw()`, `openSession()->run()`, or the same stored in an
`auto` local first — lost the receiver's type during extraction. The callee degraded
to a bare method name, so when two classes shared a method name the call silently
resolved to whichever was indexed first (or not at all), corrupting callers / impact /
trace with a plausible-but-wrong edge.
Three parts:
- Capture C++ return types (new nodes.return_type column, schema v5): the
function_definition's `type` field, normalized — smart-pointer pointee unwrapped,
void/primitives dropped.
- Preserve the inner-call receiver in extraction: a C/C++ field_expression whose
receiver is itself a call is encoded `inner().method` instead of dropping to the
bare name. Other languages keep the existing behavior.
- New resolution strategy (matchCppCallChain): infer the receiver's class from the
inner call's return type, then resolve AND validate the method on it. Handles
singletons/accessors, factories returning a different type, free-function
factories, make_unique/make_shared/new/direct construction, single-level member
chains, and namespace-qualified inner calls. A wrong inference yields no edge,
never a wrong one.
EXTRACTION_VERSION 2->3 (re-index to populate return types).
Validated on the issue repro + spdlog: node count stable (no explosion),
deterministic, and ~100 pre-existing wrong `.size()`-style edges removed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two environments that share one working tree — most concretely Windows
and WSL — can't safely share a single `.codegraph/`: the daemon lockfile
records a platform-specific pid + socket (named pipe vs Unix socket), and
SQLite locking across the WSL2/Windows filesystem boundary is unreliable,
so two daemons over one index risks corruption.
Add a `CODEGRAPH_DIR` env var (default `.codegraph`) that overrides the
per-project data directory name, so each environment keeps its own index
in the same tree (e.g. `CODEGRAPH_DIR=.codegraph-win` on Windows). The
name is resolved live and validated (rejects separators / `..` / absolute,
falling back to the default with a one-time stderr warning). Indexing and
file-watching now skip ANY `.codegraph-*` sibling so neither side trips
over the other's data.
Routes the previously-hardcoded `.codegraph` literals (db path, lockfile,
error log, watcher ignore, file-scan skip, installer) through the
resolver. No extraction-version bump — index content is unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TypeScript service/RPC contracts written as a tuple of generic types —
`type List = [Service<'query_apply_record', Req, Resp>, …]` — carry their
names only as string-literal type arguments, so static extraction never
indexed them and `codegraph query query_apply_record` returned nothing.
Add a narrow TS/TSX type-alias pass that emits each tuple entry's
string-literal name as a `method` node under the alias (qualifiedName
`List::query_apply_record`), making it searchable. Scope is limited to a
direct literal arg of a generic that is a direct tuple element, with a
valid-identifier filter — so utility types (Pick/Omit/Record), deeper
nested generics, and route paths produce no noise.
Bumps EXTRACTION_VERSION so existing indexes get a re-index hint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes codegraph_node a drop-in faster Read for indexed source files (file-read mode: <n>\t<line> like Read, offset/limit, + blast-radius header; symbolsOnly for the map). Fixes the old file-view dropping imports/line-numbers. #383/#527 preserved. Validated by A/B: explore/node already return source + line numbers, so Read=0 when used. Includes the A/B eval harness scripts. Full suite green (1270).
* feat(mcp): steer agents to codegraph during implementation, not just Q&A
Two changes targeting agents that reach for Read during edits instead of codegraph:
1. Reframe the agent-facing steering (server-instructions + codegraph_node/explore
descriptions): drop "consult BEFORE ... not during"; position codegraph_node as
the Read upgrade for a named symbol (verbatim current on-disk source, safe to
Edit from, + caller/callee trail), explore PRIMARY / node SECONDARY, with the
"cached intelligence — better context, fewer tokens" framing.
2. File-view mode: codegraph_node now accepts a `file` with no `symbol` and returns
that file's symbol map + graph role (its dependents), plus verbatim bodies with
includeCode — so it can displace a path-keyed Read, not just a symbol lookup.
Resolves a path or basename; dedups nested members; budget-capped.
To be A/B'd on an implementation task before shipping (per the retrieval doctrine:
steering changes must be measured, not assumed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): note codegraph_node file-view + implementation steering
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Attached to shared daemon" line is benign INFO, but it was written to
stderr — and MCP hosts render all server stderr at error level (and append an
`undefined` data field), so on every session start a healthy attach showed up
as `[error] … undefined`. It is now gated behind CODEGRAPH_MCP_LOG_ATTACH=1:
silent by default, opt-in for debugging daemon attach. Both attach sites
(runProxy + connectWithHello) route through one helper. The daemon integration
tests opt the harness into the log so their attach assertions still observe a
successful attach.
Re-applies the approach from #640 by @mturac.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): resolve symlinks in path validation to block out-of-root reads (#527)
validatePathWithinRoot was purely lexical (path.resolve + startsWith), so an
in-repo symlink whose logical path is inside the project root but whose real
target escapes it passed validation — and both content-serving read sinks
(codegraph_node includeCode, codegraph_explore source) then readFileSync'd it,
leaking out-of-root file contents (e.g. ~/.ssh, /etc) to the agent.
Add a realpath layer: after the lexical check, resolve symlinks on both the
candidate path and the root and re-compare, rejecting anything whose real path
escapes the root. An in-root symlink is still allowed (no over-blocking).
Comparison is case-insensitive on Windows (NTFS + realpath casing). Not-yet-
existing paths (ENOENT) fall back to the lexical result so about-to-be-written
files still validate; other resolution errors reject.
Removes the dead, never-called isPathWithinRoot / isPathWithinRootReal helpers
(the latter a footgun — it returned true on realpath failure). Adds RED->GREEN
tests: in->out file/dir symlinks rejected, in->in allowed, ../ rejected, ENOENT
allowed, plus an end-to-end test proving getCode no longer serves an out-of-root
file reached through a dir symlink.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): note the #527 symlink path-escape fix
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spring `application.{properties,yml}` keys (and Shopify Liquid `{% schema %}`
blocks) were storing the config VALUE in the node docstring, and
`codegraph_explore`'s source section re-read the raw `key = value` line off
disk — so a secret committed to a config file (DB password, API key, JDBC URL
with embedded credentials) could be pushed into an agent's context via
explore/node output without the agent ever opening the file.
Config-leaf nodes (`kind: 'constant'` in a config language) now surface the KEY
only, via a shared `isConfigLeafNode` predicate applied at both surfacing
paths: the value is dropped from extraction, `getCode`/`includeCode` returns
the key instead of the file line, and explore excludes config leaves from
source rendering. The predicate can't match real code (real constants are
ts/java/go/…), so `@Value`/`@ConfigurationProperties` resolution and impact are
unaffected. Adds a regression test asserting a planted secret never appears in
`codegraph_explore` / `codegraph_node` output while the keys still resolve.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vendor tree-sitter-c-sharp 0.23.5 (ABI 15) for C#, replacing the bundled ABI-13
build that dropped primary-constructor classes. Adds native primary-ctor
parsing, primary-ctor parameter dependency edges, return-type extraction via the
renamed `returns` field, and a preParse that blanks `#if` directive lines the
new grammar mis-parses inside enum bodies. Validated on MediatR / eShopOnWeb /
Newtonsoft.Json + full suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>