mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-09-19 07:34:57 +08:00
feat/mcp-default-toolset
468 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f15d9d451a |
feat(mcp): default tool surface trimmed to 4 — explore, node, search, callers
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>
|
||
|
|
f9fcc2cd6a |
feat(mcp): unindexed sessions go quiet — empty tools/list + inactive instructions, no-error policy (#769) (#817)
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> |
||
|
|
0682681175 |
chore(agent-eval): standing A/B model policy — sonnet + high effort, never Opus/Fable (#816)
All agent A/B arms now run claude --model sonnet --effort high by default (MODEL/EFFORT env overrides exist). Sonnet is the deliberate floor model: codegraph's users attach whatever host they already run (Cursor Composer, Gemini, ...), and a stronger model's tool-use masks the salience problems a weaker one exposes — what lands on Sonnet generalizes up; Opus/Fable-only wins don't generalize down. Policy recorded in CLAUDE.md's validation methodology; 11 hardcoded --model opus call sites across 9 eval scripts switched to the env-overridable default. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
823ffd1c3d |
feat(extraction+resolution): Astro support — frontmatter/template extraction + src/pages routes (#768) (#815)
.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>
|
||
|
|
763ee9c825 |
fix(resolution): Svelte/Vue component resolvers get the #764 ambiguity rule (#814)
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> |
||
|
|
222af6b87c |
fix(mcp+resolution): stop conflating same-named symbols across monorepo apps (#764) (#813)
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> |
||
|
|
dce61a5f4a |
fix(extraction): qualified Type::member refs skip the name gate — no-import references resolve (#812)
`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>
|
||
|
|
1f15f93feb |
feat(extraction): PHP string/array callables + Ruby lifecycle-hook symbols (#811)
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> |
||
|
|
38095aa95b |
feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
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> |
||
|
|
38eb4e688c |
fix(extraction): classify TS/JS class fields by value — properties, not methods (#808) (#809)
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>
|
||
|
|
8a114ba53c |
feat(extraction): capture function-as-value — callback registration sites in callers/impact (#756) (#807)
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>
|
||
|
|
0df9246752 |
fix(extraction): capture & clean docstrings across all README languages (#780) (#806)
* 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> |
||
|
|
0b1a2eed97 |
fix(mcp): treat a stdin 'error' as shutdown so the server can't orphan/spin (#799) (#805)
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> |
||
|
|
d0e649969a |
fix(graph): treat class instantiation as a caller/callee edge (#774) (#804)
`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> |
||
|
|
9a0f144770 |
fix(directory): self-heal a stale .codegraph/.gitignore so daemon.pid is ignored (#788) (#802)
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> |
||
|
|
c39b4b938e |
docs(readme): fill framework-coverage gaps — add Play, Vue/Nuxt, Scala (#798)
The framework story was missing several supported frameworks: - Play (Scala/Java) — absent from both the Framework-aware Routes table and the routing-coverage line. Measured 76.3% (106/139 routes resolved to a handler) across the 31 verb-route apps in playframework/play-samples; every miss is Play's framework-provided `Assets` controller (vendored library code, not app source). Slots into the convention-ceiling bucket. - Vue Router / Nuxt — recognized (file-based pages/, server/api/, middleware) but missing from the routes table. - Scala + Vue — missing from the "20+ Languages" highlight. File-based routers (SvelteKit, Vue/Nuxt) have no separate handler edge — the page IS the handler — so their coverage is the fair-coverage language figure (Svelte/SvelteKit 100%, Vue/Nuxt 93.5%), now cited explicitly. Existing framework numbers left untouched (they were measured ad-hoc; a fresh re-measure would shift them and isn't part of this gap-fill). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7b7c8b4e8 |
docs(readme): update Pascal/Delphi coverage 75.7% → 77.4% (#797)
The paren-less call extraction (#793) and free-routine attribution (#795)
added real call coverage on PascalCoin. Controlled A/B on a fresh clone,
same source-file filter, only the build differing:
baseline (pre-Pascal-work,
|
||
|
|
0b3f3f969c |
docs(design): Pascal free-routine call attribution fixed (#795) (#796)
Records the second Pascal call-coverage follow-up (#795): a free routine defined only in the implementation section now gets a function node so its body's calls attribute to it, not the file. EXTRACTION_VERSION 18. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dac00e7d44 |
fix(pascal): attribute a free routine's calls to it, not the file (#795)
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> |
||
|
|
5342f7a93e |
docs(design): Pascal paren-less method calls now extracted (#793) (#794)
Updates the chained-call design doc: the Pascal paren-less-call follow-up is done (#793) — `Obj.Free;` / `TFoo.GetInstance.DoIt;` are now extracted (scoped to statement position so field/property accesses aren't mistaken for calls). PascalCoin +1131/-1. EXTRACTION_VERSION 17. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
35dce04e1f |
feat(pascal): extract paren-less method calls (Obj.Free; / TFoo.GetInstance.DoIt;) (#793)
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> |
||
|
|
4c35b72136 |
docs(design): Pascal/Delphi chained calls shipped (#791) — 13 languages (#750) (#792)
Updates the chained-call design doc: Pascal moves from "blocked" to covered (#791) — the earlier "blocked" read was wrong, caused by probing only the paren-less form. 13 languages now shipped; EXTRACTION_VERSION 16. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af56f3539d |
fix(pascal): resolve chained factory calls TFoo.GetInstance().DoIt() (#750) (#791)
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> |
||
|
|
a4d19a5ed8 |
docs(design): record the chained static-factory call resolution mechanism (#750) (#787)
A checked-in design doc for the #645/#608/#750 chained-call mechanism — the permanent, discoverable record the work previously lacked (it lived only in git history, the tracking issue, and an untracked scratch handoff). Covers the 3-part mechanism, the three shared resolvers + receiver styles, the per-language coverage matrix (12 shipped with A/B results), the conformance pass, and the full 21-language README classification (incl. why TypeScript + Luau were skipped and Pascal is blocked). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d21d2dfa50 |
fix(objc): resolve chained message-send calls [[Foo create] doIt] (#750) (#786)
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> |
||
|
|
16c73e2b0e |
fix(dart): resolve chained static-factory / constructor calls Foo.create().bar() (#750) (#762)
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> |
||
|
|
2f96f58cbb |
fix(scala): resolve chained static-factory/apply calls Foo.create().bar() (#750) (#761)
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> |
||
|
|
ccced9e358 |
fix(go): resolve chained factory-function calls New().Method() (#750) (#760)
* 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> |
||
|
|
5805f01957 |
fix(rust): resolve chained associated-function calls Foo::new().bar() (#750) (#757)
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> |
||
|
|
7c7f0dd56f |
fix(swift): resolve chained static-factory/fluent calls + nested-extension naming (#750) (#755)
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> |
||
|
|
48d4654e8d |
feat(resolution): conformance-aware chained-method resolution (#750) (#754)
* 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> |
||
|
|
aa07dc59d4 |
fix(csharp): resolve chained static-factory calls Foo.Create().Bar() (#750) (#753)
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> |
||
|
|
3e04650850 |
fix(kotlin): resolve chained companion-factory calls Foo.getInstance().bar() (#750) (#752)
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>
|
||
|
|
7f6bdf7ad1 |
fix(java): resolve chained static-factory calls Foo.getInstance().bar() (#750) (#751)
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> |
||
|
|
eb5960b535 |
fix(php): resolve chained static-factory calls Cls::for($x)->method() (#608) (#749)
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> |
||
|
|
75ae1e8bd9 |
fix(search): down-weight the project name in ranking — completes #720 (#748)
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> |
||
|
|
afec1282e1 |
fix(search): score path relevance per query word, not per sub-token (#720) (#745)
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> |
||
|
|
5b3f5e36db |
fix(go): attribute calls inside top-level closures to the var, not the file (#693) (#744)
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>
|
||
|
|
35b44e242c |
fix(scan): don't abort indexing on a non-UTF-8 or unparseable .gitignore (#682) (#743)
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> |
||
|
|
6e2a24d96a |
fix(extraction): map PHP include/require to file→file dependency edges (#660) (#663)
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> |
||
|
|
fd03f31b2c |
fix(cpp): resolve calls through singletons/factories/chained getters (#645) (#742)
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> |
||
|
|
a56d9e6941 |
feat(directory): CODEGRAPH_DIR env var to override the index dir name (#636) (#741)
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> |
||
|
|
636d9fcb7d |
feat(extraction): index string-literal names in generic tuple type aliases (#634) (#740)
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> |
||
|
|
1983590533 |
feat(mcp): codegraph_node reads files like the Read tool — offset/limit, byte-parity (#738)
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). |
||
|
|
0e2789ab71 |
docs(agent-eval): nested MCP attach is startup-latency, not a hard block (#735)
Corrects the "run non-nested only" conclusion from #734. The codegraph server is healthy (handshake ~165ms); the flakiness is that on a multi-step implementation task the agent dives into Read/grep before codegraph finishes its ~2-3s startup (worse under nested CPU contention), so it runs with no codegraph. Fix: pre-warm a persistent daemon (high idle timeout) + skip the startup re-exec (CODEGRAPH_WASM_RELAUNCHED=1) so claude connects before the agent's first turn. claude's init snapshot can show status:"pending" even when it then connects — judge by actual codegraph usage, not the init line. ab-new-vs-baseline.sh now bakes in the pre-warm + skip-re-exec. Validated: a clean A/B showed the new build's agent used codegraph 2x / 5 Reads vs the baseline's 0 / 8 on the same fully-implemented task. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
28c5268ee3 |
docs(agent-eval): evals must run non-nested + add ab-new-vs-baseline.sh (#734)
Running scripts/agent-eval against a `claude -p` spawned from within a Claude Code session (nested, e.g. from a Bash tool call) makes the codegraph MCP attach unreliable: the server is healthy (full handshake ~165ms) but the nested client marks it status:"pending"/0-tools under CPU/timing contention, so the agent silently runs with no codegraph. NO_DAEMON + `< /dev/null` don't fix it — it's the nested client, not the server. Documented in CLAUDE.md's validation methodology. Adds ab-new-vs-baseline.sh: A/Bs a retrieval/steering change as new-build vs baseline-build (both codegraph-on, isolating the change — vs run-all.sh's with-vs-without), on a throwaway copy of an indexed repo. Run it in a real terminal. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7175dc456c |
feat(mcp): steer agents to codegraph during implementation + file-view node mode (#733)
* 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> |
||
|
|
10defecc4b |
fix(mcp): silence the daemon-attach log by default (#618) (#725)
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> |
||
|
|
7fd8b4c185 |
fix(security): resolve symlinks in path validation to block out-of-root reads (#527) (#724)
* 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> |
||
|
|
112e278b5c |
fix(security): index config files by key only, never surface values (#383) (#722)
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>
|