d289bf84d38e07001b132ecc20837466029395d9
561
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e53968cae8 |
fix(resolution): gate the Lua/Luau annotation pattern against method-call self-match (#1124) (#1131)
Lua method-call syntax (lg:Log()) is byte-identical to the Luau type-annotation shape (lg: Logger), and the receiver-type scan starts on the call's own line — so any PascalCase method call self-matched as "type = Log" before the scan reached the real declaration, silently dropping the calls edge whenever two or more classes shared a method name. The annotation pattern now rejects a capture followed by any of Lua's three call forms; its leading [\w.] lookahead alternative prevents backtracking from shrinking the capture to dodge the gate. Gated rather than dropped: the pattern is the only type source for Luau typed params and annotated locals whose initializer isn't T.new(). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf86fe8198 |
fix(resolution): extend typed-parameter receiver inference to Rust/Go/Dart/PHP (#1125) (#1130)
Completes the #1125 fix. The same typed-parameter gap fixed for TS/JS existed in every other language whose localReceiverTypePatterns only matched keyword-anchored locals (let/var/:=/= new) and never the bare parameter form: - Rust: the `:`-annotation pattern required `let`, so `fn use(lg: &Logger)` didn't match. Dropped the `let` anchor (still covers `let lg: Logger`), keeping the `&?mut?` handling — now covers params and closures `|lg: T|`. - Go: only `lg := T{}` / `var lg T` matched; a parameter/method-receiver `func use(lg Logger)` / `func (l Logger) M()` (name-before-type, no keyword) didn't. Added a PascalCase-guarded `ident Type` pattern — the guard plus the existing enclosing-scope bound (excludes package-level struct fields) keep the keyword-free shape from matching unrelated pairs. - Dart: the type-before-name pattern's trailing `[=;]` missed a parameter's `)`/`,`. Widened to `[=;,)]`, mirroring Java/C#. - PHP: only `$lg = new T` matched; a typed param `function use(Logger $lg)` (also `?Logger`, `\App\Logger`, `&$lg`, `catch (E $e)`) didn't. Added a type-before-$var pattern. Reserved words can't be class names, so the looser lowercase-allowing capture yields no wrong edges. Every pattern still relies on resolveMethodOnType validating the inferred type actually declares the method (no edge on a mis-inference) — the same safety net the already-covered languages use. Verified with a deterministic probe: all four now disambiguate two same-named methods via the typed param (Java + Kotlin as passing controls), full suite green (1930), no regressions. Adds a parameterized regression test (Rust/Go/Dart/PHP), associating method to type by qualifiedName so it holds where the method sits outside the type's line range (Rust impl, Go decl). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
385001398b |
fix(resolution): infer typed-parameter receivers in TS/JS (#1125) (#1129)
The local-variable receiver-type inference from #1108/#1110 covered typed
parameters for every language except TypeScript/JavaScript (+ TSX/JSX). The
TS/JS `:`-annotation pattern required a leading `const|let|var`, so it only
matched a local's own annotation (`const lg: Logger`) and never a bare
parameter (`function use(lg: Logger)` / `(lg: Logger) =>`). With a second
class sharing the method name — the case where a same-name fallback can't
paper over it — `lg.log()` resolved to no edge, dropping it from callers and
impact/blast-radius. TS/JS is the most common language pair in the userbase,
so this was a real precision gap.
Replace the keyword-anchored pattern with the keyword-free
`\b${r}\b\s*:\s*([A-Z][\w.$]*)`, mirroring Kotlin/Swift/Scala. It's a strict
superset (still matches `const lg: Logger`) plus the typed-parameter case,
and the capture stops at `<` so a generic-typed param
(`repo: Repository<User>`) still yields `Repository`. resolveMethodOnType
already validates the inferred type declares the method, so the looser match
produces no edge on a mis-inference — the same safety net the other
languages rely on; Swift already ships this identical bare-colon pattern with
the same theoretical ternary/dict-literal exposure.
Adds a regression test using two ambiguous classes + typed params, asserting
each call routes to its OWN class's method (verified to fail without the fix
and pass with it — a single-class version would pass either way via the
same-name fallback, which is why the collision is load-bearing).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7c7514f43f |
fix(sync): degrade auto-sync on a persistent non-lock sync failure (#1127) (#1128)
FileWatcher.flush() bounded only two failure modes — lock contention (backoff + degrade past MAX_LOCK_RETRIES) and watch-resource exhaustion (degrade at setup). Its generic catch branch — any *other* sync error — reset the only circuit breaker (lockRetryCount = 0) and fell through to scheduleSync() at the normal debounce cadence, forever, with no backoff and no degrade(). The trigger is realistic, not synthetic: CodeGraph.sync() runs the whole extract -> resolve -> maintenance pipeline inside try/finally(release) with no catch, so a deterministic failure (a tree-sitter extractor that crashes on one file, SQLITE_FULL, an OOM in batched resolution) propagates straight into that unbounded branch — wedging a long-running daemon/MCP session into ~1,800 failing syncs + log lines/hour while the auto-update guarantee is silently dead. Mirror the lock circuit breaker for the generic branch: a separate consecutive-failure counter (syncFailureRetryCount) reset only by a clean sync, exponential backoff via the shared finally, and degrade() past MAX_SYNC_FAILURE_RETRIES with an actionable reason naming the underlying error. degrade() -> onDegraded/isDegraded() is what surfaces the dead guarantee (the staleness banner already consumes it) — a lighter flat-retry would keep it hidden, which is the core of the #876/#1127 complaint. Reset-on-success means a transient hiccup never degrades. The lock path is behaviorally unchanged: in any pure-lock scenario syncFailureRetryCount stays 0, so Math.max(lockRetryCount, syncFailureRetryCount) and the degrade threshold behave exactly as before. Renamed MAX_LOCK_RETRY_DELAY_MS -> MAX_RETRY_BACKOFF_MS (shared cap). Adds two regression tests mirroring the lock-contention ones: a persistent non-lock failure degrades past the budget; a transient one recovers without degrading. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
358f400c40 |
feat(resolution): local-variable method calls in Lua, Luau, R, Pascal (#1112) (#1113)
Extends the local-variable receiver-type inference (#1108/#1110) to the remaining supported languages with object-method calls. An empirical sweep found Objective-C, Svelte, Vue, and Astro already resolved `localVar.method()` (ObjC via message-send handling; the template langs ride the TypeScript path), leaving Lua, Luau, R, and Pascal. Lua/Luau/R were a resolution gap, not extraction: the call ref IS extracted (`lg:log`, `lg$log`), but (1) the resolver's fast pre-filter `hasAnyPossibleMatch` only understood `.`/`::` separators, so a `:`/`$` ref was dropped before any strategy ran, and (2) matchMethodCall only parsed `.`/`::` receivers with no local-var inference for these langs. Fixes: pre-filter now checks the member/receiver around `:` and `$`; matchMethodCall recognizes `lg:log` / `lg$log` and routes them through the same inference + validated resolveMethodOnType path; and inference patterns are added for Lua/Luau (`local x = T.new()` / `T()` / `x: T`), R (`x <- T$new()`), and Pascal (`var x: T` / `x := T.Create`). Pascal statement-form calls (`obj.Method;`) now resolve via the new inference pattern. The assignment-RHS parameterless form (`x := obj.Method`) is deliberately left as a field read by the existing Pascal extractor — an intentional field-vs-call ambiguity tradeoff — so it stays out of scope. Validated with single-file and two-file same-name repros per language (resolves to the right method; two-file is same-file-correct, #1079). Adds all four to the local-variable inference test matrix. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3424ff36c5 |
fix(extraction/ruby): build receiver.method calls so instance calls resolve (#1110) (#1111)
The Ruby extractor dropped the method name from a `receiver.method` call: `lg.log()` was recorded as a call to `lg` (the bare receiver), which matches no symbol, so the reference resolved to nothing and no method edge was ever produced. A Ruby method invoked through a receiver had no recorded callers and was invisible to impact/blast-radius and explore flow traces. This is the Ruby-specific blocker noted in #1108 — that local-variable type-inference fix couldn't help Ruby because the call reference itself was missing. extractCall recognized receiver-bearing calls by the `object`/`name`/ `function` fields other grammars use; tree-sitter-ruby's `call` node uses `receiver` + `method`, so it fell through to the generic fallback that takes the first named child (the receiver) as the callee. Handle Ruby `call`/`method_call` explicitly: build `receiver.method`, keep bare `foo(...)` as the method name, emit `Foo.new` as an `instantiates` ref, and give a capitalized (constant) receiver a `references` edge so a class used only via its class methods still records a dependent. With this plus #1108, `lg = Logger.new; lg.log` resolves `lg.log` to `Logger#log`, and the two-file same-name case is same-file-correct (#1079). Adds Ruby to the local-variable inference test matrix plus a focused test asserting `Foo.new` stays an instantiation. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed64db08b4 |
feat(resolution): infer local-variable receiver types across languages (#1108) (#1109)
Instance calls through a local variable — `const lg = new Logger();
lg.log();` — only resolved to the method in C++. Every other language
produced no `calls` edge, because the resolver had no way to learn the
receiver variable's type, so such calls were missing from callers,
impact/blast-radius, and explore flow traces.
Local variables aren't indexed as nodes (node-explosion), so — like the
existing C++ inferrer — this reads the enclosing function's source and
matches the receiver's declaration/initializer to recover its type, then
hands it to resolveMethodOnType. That validates the method actually
exists on the inferred type, so a mis-inference yields no edge, which is
what lets the per-language patterns stay simple. The scan is bounded to
the enclosing scope so a same-named variable in another function can't
leak in.
Generalizes the C++-only path in matchMethodCall into a language dispatch:
C++ keeps its dedicated header-aware inferrer; a new shared
inferLocalReceiverType covers TypeScript, JavaScript, Python, Java, C#,
Kotlin, Swift, Go, Rust, Dart, Scala, and PHP, matching each language's
declaration shapes (`= new T`, `= T(...)`, `= T.new`, `let x = T{}`,
`x := T{}`, `T x = ...`, `x: T`, etc.). For Java/Kotlin an import FQN
still pins which same-named class is meant (#314); other languages fall
back to the call-site's own file (#1079).
Ruby is not covered: its extractor emits no `receiver.method()` call
reference in the first place, so there is nothing for resolution to
resolve — a separate extraction-layer gap.
Adds a parameterized end-to-end test covering all twelve languages. Full
suite green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
63bc0fd037 |
fix(resolution): resolve same-named methods to the call site's own file (#1079) (#1107)
When two files each declared a same-named class with a same-named method
(e.g. `class Logger { void log(); }`), a call resolved to whichever
definition was indexed first — so a call in `b/svc` wrongly targeted
`a/svc`, mixing up that method's callers and blast radius.
The reported case was C++ instance calls, but the underlying pattern —
"multiple same-named candidates, pick the first-indexed, ignore the call
site's file" — lived in three resolution paths, each firing for a
different call shape and affecting different languages:
- `obj.log()` instance -> resolveMethodOnType (C++)
- `Logger.log()` class receiver -> matchMethodCall Strategy 1/2/3
(Python, TypeScript, Java, C#)
- `Logger::log()` qualified -> matchByQualifiedName (C++, Rust)
All five sites now share one helper, `preferCallSiteFile`, that prefers
a candidate declared in the call site's own file when a name is
ambiguous. It runs after the `preferredFqn` block in resolveMethodOnType,
so Java/Kotlin import disambiguation (#314) — whose target is
intentionally in another file — is unaffected. The helper is a no-op
when there are fewer than two candidates or none share the call site's
file, so the common single-definition case is unchanged.
Adds 8 tests under `Same-name method disambiguation (#1079)`: the
`preferCallSiteFile` contract, resolveMethodOnType precedence (including
a guard that an import FQN still beats the same-file preference),
`matchByQualifiedName` disambiguation, and end-to-end index tests for the
C++ instance, TypeScript static, and C++ qualified call shapes.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
43a6fa68f6 |
fix(graph): complete edge sets & correct node limits in traversal (#1086, #1087, #1088, #1089, #1090)
Three root defects in src/graph/traversal.ts (reported by @inth3shadows as #1086–#1090): - Depth guard returned before visited.add → duplicate callers/callees at maxDepth=1 and getImpact loop disagreement. - Dedup gate also gated edge collection → traverseBFS dropped a parallel edge; getImpact dropped a direct incoming dependency edge. - limit checked per-frame not per-add → high-degree node overshot opts.limit in traverseBFS and dfsRecursive. traverseBFS now collects every distinct edge among kept nodes (deduped on edge identity), enqueues each node once, and caps per-add. getCallers/getCallees/getImpactRecursive mark visited before the depth check; getImpactRecursive records the incoming edge unconditionally and unifies its loops on visited. 7 regression tests in graph.test.ts, each failing on the pre-fix code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed39233f1a |
fix(index): yield during resolution so the liveness watchdog can't kill a valid large index (#1091) (#1105)
The #850 liveness watchdog SIGKILLs a process whose main-thread event loop stalls past its window (60s default). It was extended to `index`/`init` in #999, but reference resolution and callback-edge synthesis run synchronously on that same thread — so on a large repo a legitimate, in-progress index gets killed, and users had to disable the watchdog entirely (CODEGRAPH_NO_WATCHDOG=1). Make the long synchronous spans yield cooperatively so the heartbeat keeps firing during real work, while a genuinely wedged span (which never reaches a yield) still trips the watchdog: - synthesizeCallbackEdges yields between its whole-graph passes, and the heavy scanners (closure-collection, event-emitter, JSX-child, object-registry, field-channel) yield within their loops; - batched resolution sub-chunks each batch with yields; - the deferred chained-call and this-member post-passes yield per ref. Behaviour-preserving — only timing changes; node/edge counts are identical. Validated end-to-end with the real watchdog armed at the default 60s: the released build is SIGKILLed partway through indexing the Swift compiler (27k files, ~1.1M edges) and the TypeScript compiler, while the fixed build indexes both to completion. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6dd5512d8d |
fix(windows): set windowsHide on remaining child spawns to stop console flash (#1092) (#1104)
On Windows, a black console (conhost) window flashed briefly when CodeGraph ran as a background MCP server. Several child spawns were missing `windowsHide: true`, so Windows created a visible console for the child: - scripts/npm-shim.js — launching the bundled runtime (every server start / daemon-idle reconnect) and the self-heal `tar` extraction of a missing platform bundle. - src/reasoning/login.ts — the detached `cmd /c start` browser open. - src/upgrade/index.ts — package-manager spawn (console-attached, so no flash in practice, but set for uniformity: every child spawn now hides). The daemon spawn (#411) and all git execFileSync sites already set it; this closes the remaining gaps. Adds an all-platforms source guard to __tests__/npm-shim.test.ts asserting every spawn in the shim sets windowsHide. Closes #1092 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
00765200d8 |
fix(extraction): broaden the curated C++ inline-macro library list (#1103)
* fix(extraction): broaden the curated C++ inline-macro library list Since #1102 the post-parse salvage already recovers the NAME for any macro, so adding a library now buys full return-type recovery for it. Extend the curated list across the major C++ ecosystem: Mozilla/SpiderMonkey, Protobuf, {fmt}, Hedley + nlohmann/json, GLM, Bullet (SIMD_FORCE_INLINE), Skia, OpenCV, EASTL, Cocos2d-x, Chromium/WebKit (NEVER_INLINE), GLib, SQLite, and the unambiguous Windows calling conventions (WINAPI / APIENTRY / STDMETHODCALLTYPE / WINAPIV — which sit between the return type and the name, so blanking them recovers the return type, e.g. `HRESULT WINAPI Foo()` -> Foo : HRESULT). Every entry is an exact, curated token matched only in specifier position, so a real all-caps return type is never touched. Anything still missed keeps its name via the universal salvage. CARLA control unchanged (440->6 mangles, 0 regressions — none of these libs appear there, confirming no collateral). Eleven representative full-recovery tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note broadened C++ inline-macro library coverage (#1103) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cb20a3bf7f |
feat(extraction): universal recovery of macro-mangled C/C++ function names (#1102)
* feat(extraction): universal recovery of macro-mangled C/C++ function names The curated inline-macro blank list (#1100/#1101) can't enumerate every library's macro. Add a universal post-parse net so a function is findable by name regardless of which macro decorates it, plus a batch of common libraries to the curated list for full name+return-type recovery. - recoverMangledCppName: after extraction, recover the real identifier from a name still mangled by an un-blanked macro (`MACRO Ret name(…)` misparses to "Ret name"). It's a new `recoverMangledName` extractor hook wired only onto C/C++, applied to every name they produce. Safe by construction: it only touches an already-mangled name (an internal space that isn't a legit `operator …`/destructor), so a clean name is returned unchanged; guarded against the `Ret (name)` parenthesized-name idiom and bare primitives. Scoped to C/C++ so Kotlin/Scala backtick identifiers (which legitimately contain spaces) are never touched. - Curated list extended past UE/pugixml/Godot/Boost to Qt (Q_INVOKABLE, …), Folly, Abseil, LLVM, V8, Eigen, and rapidjson. Validated on CARLA (large UE project, 1131 C++/h files) vs the pre-fix baseline: function-name mangles 440 -> 6, 431 fixed, and — critically — 0 regressions (the salvage also recovers names that the pre-parse's own non-local error-recovery shifts would otherwise re-mangle, erasing the 7 shifts seen in #1101). The 6 residual are all the moodycamel `Ret (name)` idiom, correctly left alone. On a made-up macro with no list entry (`WEBKIT_EXPORT WTFString compute()`), the name `compute` is still recovered. Full suite green; eleven regression/safety tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note universal C++ macro-mangled name recovery (#1102) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a164ceae8b |
fix(extraction): recognize common third-party C++ inline macros, not just UE (#1101)
* fix(extraction): recognize common third-party C++ inline macros, not just UE Extend blankCppInlineMacros beyond Unreal Engine's FORCEINLINE family to the inline/linkage macros that vendored third-party libraries define and that mangle function names the same way: - pugixml: PUGI__FN / PUGI__FN_NO_INLINE (before the return type) and PUGIXML_FUNCTION (linkage macro, between return type and name — the blank mechanism handles both positions). - Godot: _FORCE_INLINE_ / _ALWAYS_INLINE_. - Boost: BOOST_FORCEINLINE / BOOST_NOINLINE. - Generic cross-ecosystem hints: ALWAYS_INLINE / FORCE_INLINE / NOINLINE. The list now drives a single generated alternation (longest-token-first), so adding a codebase's macro is a one-line change. Still curated exact tokens in specifier position only — a real all-caps return type like `HRESULT DoIt()` is never touched (verified by controls). Validated on CARLA (large UE project, 1131 C++/h files): function-name mangles 440 -> 16 (428 fixed). The 16 residual and 7 clean->mangled shifts are all in third-party vendored files — chiefly pugixml.cpp, a 12k-line macro amalgamation where error recovery is non-local, so blanking one of several *stacked* macros (PUGI__FN + PUGI__UNSIGNED_OVERFLOW …) shifts an already-imperfect extraction. Normal C++/UE code (ActionRoguelike, ALS) sees zero regressions — blanking a macro there only helps. Chasing pugixml's internal attribute macros is left out of scope. Seven regression tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note third-party C++ inline macro recognition (#1101) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9b2ce1c8f6 |
fix(extraction): recover C++ function names prefixed by an inline-specifier macro (#1100)
* fix(extraction): recover C++ function names prefixed by an inline-specifier macro An unknown inline-specifier macro before a function's return type (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery: the macro was read as the return type and — for a non-primitive return — the return type was glued onto the name, so the function was indexed as `"FString GetName"` instead of `GetName`, unfindable by name and with no caller links. This is pervasive in Unreal Engine, where inline helpers are written `FORCEINLINE <ret> <name>(…)` (e.g. ALS's `FORCEINLINE FString GetEnumerationToString`). Add `blankCppInlineMacros`, a preParse that blanks the known UE inline macros (`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`) with equal-length spaces so byte offsets stay exact and the declaration parses as an ordinary function — recovering both the real name AND the return type. This is the same recover-don't-drop approach as blankCppExportMacros (#946/#1061), and the two are composed into the cppExtractor preParse. Matched tightly (exact known tokens, only in specifier position — followed by the identifier that starts the return type/name), so ordinary identifiers, real all-caps return types (`HRESULT DoIt()`), string literals, expression uses, and longer words (`FORCEINLINE_COUNT`) are untouched — verified by controls. C++-only; Kotlin/Scala re-index byte-for-byte identical. Five regression tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note C++ inline-specifier-macro function name fix (#1100) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
712a406726 |
fix(extraction): correct C++ reference-return and conversion-operator method names (#1096)
* fix(extraction): correct C++ reference-return and conversion-operator method names Two pre-existing C++ name-extraction bugs surfaced while validating the #1093 forward-declaration fix against real Unreal Engine repos (ActionRoguelike, ALS): 1. Inline methods/functions returning a reference were named after the whole declarator. `const int& getRef() const {…}` parses with a reference_declarator wrapping the function_declarator; extractName unwrapped pointer_declarator but not reference_declarator, so the method was named "& getRef() const" instead of "getRef" — polluting search and breaking caller linkage. Ubiquitous in UE headers (`const FGameplayTagContainer& GetActiveTags() const`). Now the reference wrapper is unwrapped alongside the pointer wrapper. 2. User-defined conversion operators were named with their full declarator — `operator EALSMovementState() const` — instead of `operator EALSMovementState`, so they didn't match the symbolic-overload style (`operator+`) and carried `() const` noise. The operator_cast declarator is now named `operator <type>`. Both are additive and C++-scoped (reference_declarator / operator_cast are C++ grammar nodes). Pointer, value, and out-of-line reference returns, and symbolic operator overloads, are unchanged. Six regression tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note C++ reference-return and conversion-operator name fixes (#1096) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f856f7ae49 |
fix(extraction): skip bodiless C++ forward declarations (#1093) (#1095)
A `class Foo;` forward declaration parses as a bodiless class_specifier. extractStruct (#831) and extractEnum already skip their bodiless forms, but extractClass did not — so every forward decl across dozens of headers minted a phantom bodiless `class` node that competed with, and could be picked as the blast-radius representative over, the single real definition. Add an opt-in `skipBodilessClass` extractor flag (set only on cppExtractor) and skip a bodiless class node when it's set, mirroring the struct/enum skip. The flag keeps this C/C++-scoped: languages where a bodiless class is a complete definition (Kotlin `class Empty`, Scala `case object`/`trait`) leave it unset and are unaffected. The body is now resolved once at the top of extractClass and reused for the member walk. Regression tests cover the collapse to a single definition, elaborated-type references creating no phantom, and Kotlin/Scala staying indexed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9684b3b5a5 |
fix(index): rebuild a poisoned/oversized index by recreating the DB, not row-DELETE (#1067) (#1073)
Follow-up to #1065/#1066. Those stopped a *new* index from scanning an ignored gitlink corpus, but a project that had already built the multi-GB graph before upgrading still couldn't recover: `codegraph index` printed only "Indexing project" and was then SIGKILLed (137) by the #850 watchdog ~60s later, before scanning even started. Root cause is not the scanner. `index` cleared the old graph with a synchronous `DELETE FROM nodes/edges/files`. `nodes` carries an FTS5 `AFTER DELETE` trigger, so deleting ~1.6M rows fires ~1.6M FTS delete-markers — O(rows), and it grows the WAL further before it can finish. A deterministic probe puts the DELETE-clear at 20.4s on 1.5M synthetic nodes (WAL 1.16->2.14GB); at the report's denser ~2.6KB/node WAL that crosses the 60s main-thread watchdog. `open()` was never the wedge. A full re-index is documented as "same result as a fresh init", so make it one: discard the database files and re-initialize, instead of opening the old DB and DELETE-ing every row. - db: add removeDatabaseFiles(dbPath) — unlinks codegraph.db + its -wal/-shm sidecars (O(1) regardless of size; sidecars best-effort). - index: add CodeGraph.recreate(projectRoot) — discards the files and returns a fresh, empty instance. Never opens or migrates the poisoned DB. POSIX unlinks an open file fine (a live daemon heals via reopenIfReplaced, #925); a Windows file lock becomes an actionable "stop the daemon / remove .codegraph" error. - cli: `codegraph index` now calls recreate() instead of open()+clear(); both clear() calls dropped. The public clear() API is unchanged. This also reclaims the disk the bloated db/-wal were holding. Validated: deterministic probe (DELETE O(rows) vs recreate O(1)); an end-to-end run through the built binary recovering a real 800K-node / 419MB poisoned DB in 0.3s with no wedge and the correct small graph; new unit + CLI regression tests; existing #874 index tests still green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e596c968ab |
fix(cpp): recover export-macro-annotated classes instead of dropping them (#1061) (#1070)
A C++ class annotated with an export/visibility macro between `class`/`struct` and the type name — `class MYMODULE_API UMyComponent : public UActorComponent` (the standard Unreal-Engine `*_API` pattern), or the equivalent `*_EXPORT`/ `*_ABI` macros in Qt, Boost, LLVM, etc. — makes tree-sitter read `class MACRO` as an elaborated type and the whole declaration as a function. #946 dropped the resulting phantom function, but that also discarded the recoverable class name, members, and base-class edge, so the class never entered the graph and "find subclasses" / type-hierarchy / impact-through-inheritance returned nothing for effectively every gameplay class in a UE project. Add `blankCppExportMacros` as `cppExtractor.preParse`: it blanks the macro with equal-length spaces before parsing (offset-preserving, like C#'s `blankCsharpPreprocessorDirectives`/#237), so the declaration parses as a normal class_specifier and existing extraction emits the node, members, and `extends` edge. Generalized past UE `*_API` to any all-caps export macro, with two false-positive guards: the trailing `[:{]` definition-guard (leaves elaborated var decls like `struct FOO var;` alone) and requiring the macro to be followed by the real name (leaves an all-caps class NAME such as `class FOO : public Base` alone). C++-only, so C's heavier `struct TAG var;` never reaches it. The #946 drop stays as the fallback for any residual misparse the blanking doesn't catch. Validated on google/leveldb (LEVELDB_EXPORT, 134 files): class/struct nodes 266→293, extends edges 292→359, phantom functions 588→513; every export-macro real definition flips function→class and `EnvWrapper extends Env` goes absent→present. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2b256b93e5 |
fix(explore): surface a named method's buried signature type (#1064) (#1069)
* fix(explore): surface a named method's buried signature type (change surface) (#1064) #1064: a natural-language query like "what do I need to change if I add a new parameter to NewClient" dropped the file the answer lives in (grpc-go's dialoptions.go, which defines NewClient's DialOption) and surfaced lexical namesakes instead, so the agent fell back to grep. Root cause (instrumented on grpc-go): the answer file is lexically dissimilar to the query and reachable only structurally, so it scores ~0 on every text and centrality signal and never renders. Two-part fix, both bounded and validated to not perturb flow queries: 1. Change surface — read each named method's signature-type edges from the full graph and, ONLY when the type's file is genuinely BURIED (≈0 graph mass AND no term hits), inject + rank + gate-keep + tier it. A well-connected type file is left to rank on its own merit, so this never displaces a flow file. 2. Tier de-noise by centrality — still seed every <=3-def name (RWR/flow ranking unchanged), but the named-first tier admits only the most-substantive def plus co-named defs of comparable centrality (>=25% of the top def's caller count). This keeps real overloads/wrappers (excalidraw's `mutateElement` in three files, callers 74/58/40) while dropping vastly-less-central namesakes (Go's `NewClient`: real 492 callers vs xds-pool 11, test-fake 3) that would otherwise crowd the answer file out of the tier. Validation: - Deterministic probe: dialoptions.go goes from dropped to surfaced (with the full option field set via defaultDialOptions). - Broader-repo regression check (Alamofire, excalidraw, axios): 5/6 control flow queries byte-identical to baseline; the 1 shift (Alamofire "how a request gets validated": Validation.swift -> DataRequest.swift) is lateral — DataRequest defines validate() and is a directly-relevant answer. - grpc-go agent A/B (n=2, sonnet): grep fallback eliminated (0 vs baseline 4,2). - 1845 unit tests pass. * docs(changelog): note explore signature-type surfacing (#1064) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4159539fb8 |
fix(index): respect .gitignore for tracked gitlink embedded repos (#1065) (#1066)
The gitlink discovery added in #1031/#1033 indexed a tracked 160000 gitlink even when the parent .gitignore excludes its directory, pulling a gitignored reference/benchmark corpus of git add'ed clones into the index (one report: ~138k files, 4.8 GiB, wedged "Resolving refs" watchdog). Gate both gitlink-discovery sites on the same rule the untracked-embedded path already uses: skip a gitignored gitlink unless codegraph.json includeIgnored opts it in; index non-ignored gitlinks as before. Validated real-world on macOS, Linux, and Windows. Closes #1065 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ffff9c2b56 |
fix(index): exclude Android resource XML from the index by default (#1047) (#1054)
An Android `res/` tree (layouts, value bags, drawables, menus, navigation graphs) holds only non-code resources that yield zero symbols, yet on an Android app it dominates the file count (one report: 26k XML = 97% of files, 0 symbols) — bloating the DB, slowing indexing, and padding explore/search results and file counts with entries that have nothing to find. Default-ignore the Android resource type directories (`res/layout/`, `res/values/`, `res/drawable/`, … and their `-<qualifier>` variants) at discovery, via DEFAULT_IGNORE_PATTERNS so it applies uniformly to the git index, the non-git walk, and change detection. The `res/<type>/` structure is self-identifying, so non-Android projects are untouched, and the only XML that carries symbols — MyBatis mappers under `src/main/resources/` — never lives under `res/`, so nothing useful is dropped. `res/raw/` is deliberately kept (arbitrary bundled assets), and a `.gitignore` negation re-includes anything. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9bce41b858 |
fix(explore): report the curated result count, not the raw candidate gather (#1046) (#1053)
codegraph_explore's "Found N symbols across M files." header reported
`subgraph.nodes.size` / `fileGroups.size` — the raw FTS gather. A broad
natural-language query ("publish status to the API") matches a huge pool
(260 symbols / 124 files on a 636-file repo) while only a handful clear
the relevance gate + budget and render, so the header read as "260
results to wade through" even though the correctly-ranked answer was the
few files shown.
Report instead the files whose source actually SURVIVES in the final
output (after the hard-ceiling truncation that can drop trailing
sections), summing their relevant symbols. Gather, ranking, gate, budget,
and rendering are untouched — only the header string changes. Overflow
relevant files are still named under "Not shown above", so nothing is
hidden. Adds a regression test locking header-count == rendered-sections.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4b58a6d2d0 |
fix(cli): stop rendering raw FTS score as nonsensical percentages in query (#1045) (#1052)
`codegraph query` printed `(score * 100)%` next to each hit, but `score` is an unbounded BM25/FTS relevance magnitude (relative-ranking only), so it rendered as values like "12042%" that made the output look broken. Results already arrive in rank order, so drop the score from the human-readable output entirely — matching the MCP search tool, which shows no score. The raw `score` stays in `--json` for programmatic sorting/thresholding. Also corrects the SearchResult.score doc comment, which wrongly claimed a 0-1 range. Adds an end-to-end regression test. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0d331b9017 |
fix(cli): make node symbol positional optional so node -f <file> works (#1044) (#1051)
`codegraph node` was defined with a required `<name>` positional, so commander.js rejected `codegraph node -f <file>` with "missing required argument 'name'" before the action ran — making file-read mode (the CLI face of the codegraph_node MCP tool's file mode) unreachable. The action body already handled an absent name. Make `name` optional (`[name]`), validate that a symbol or a file is supplied (friendly usage hint instead of a cryptic commander error when neither is), and guard the name-based arg branches so they never run on undefined. Adds an end-to-end regression test across all four paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0da2dcec8e |
fix(db): dedup edges with a UNIQUE identity index so INSERT OR IGNORE works (#1034) (#1050)
`insertEdge` has always used `INSERT OR IGNORE`, but the edges table carried no UNIQUE constraint — only an autoincrement PK and non-unique indexes — so `OR IGNORE` had nothing to conflict on and behaved like a plain INSERT. Whenever two extraction/resolution passes emitted the same edge (e.g. a return type captured by both a type-reference and a value-reference pass), the graph stored byte-identical duplicate rows: ~527 on this repo, inflating edge counts and letting callers/impact list the same relationship twice. Add a UNIQUE identity index on (source, target, kind, IFNULL(line,-1), IFNULL(col,-1)) — in schema.sql for fresh databases and migration v6 (dedup existing rows, then create the index) for existing ones. IFNULL folds the nullable line/col so coordinate-less edges (synthesized / file-level) dedup too; SQLite otherwise treats each NULL as distinct. Distinct call sites (same source/target/kind, different line/col) are preserved — only byte-identical structural duplicates collapse. This is the storage-layer invariant the reporter identified: it makes OR IGNORE keep its promise and catches every double-emit, present and future, rather than chasing each emitting pass. Migration v6 is deterministic (keeps the lowest id per identity group) and idempotent (IF NOT EXISTS index; no-op DELETE once unique). The DELETE's GROUP BY matches the index expression exactly so creation can't fail on a leftover pair. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2176a7a439 |
fix(extraction): record instantiates for C++ stack/brace construction (#1035) (#1049)
`instantiates` edges came only from heap `new Calculator(0)` (a
new_expression) and copy-init `Calculator c = Calculator(0)` (a
call_expression). Stack direct-init `Calculator calc(0)` and brace-init
`Widget w{1, 2}` parse as a `declaration` whose constructor arguments hang
directly off the declarator as an argument_list / initializer_list — there
is no call/new node — so the function-body walker saw no constructor
invocation and emitted no edge. A function that built objects with the
ordinary stack syntax looked like it didn't construct them, and the
dependency was missing from impact / callers.
In the body walker, a C++ `declaration` that is a stack/brace construction
now reuses extractInstantiation (a declaration's `type` field IS the
constructed class name, and extractInstantiation already strips template
args / namespace and emits the `instantiates` ref). Gated by
isCppStackConstruction, which requires BOTH a class-like type
(type_identifier / template_type / qualified_identifier — so `int x(0)`
and `auto z = …` are excluded) AND a declarator carrying args
(argument_list / initializer_list — so default `Calculator c;` and the
most-vexing-parse `Calculator c();` are excluded). The edge targets the
class node, not the same-named constructor method.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f4e03e9cdc |
fix(extraction): resolve C++ inheritance from templated base classes (#1043) (#1048)
A C++ class deriving from a template — `class Derived : public Base<int>`, a CRTP base `class App : public CRTPBase<App>`, a struct inheriting a template, or a templated base mixed into a multi-base clause — recorded its base as the full instantiation text (`Base<int>`). That never name-matched the template, which is indexed as the bare node `Base`, so the `extends` edge never resolved and the derived class looked like it inherited from nothing — callers/impact analysis stopped at the boundary. Strip the template arguments from the base-type reference name in the `base_class_clause` handler via a new `stripCppTemplateArgs` helper: it removes every balanced `<…>` group (any nesting/position), so `Base<int>` → `Base` and `ns::Tpl<int>` → `ns::Tpl`. The remaining qualified head is exactly what the non-templated base case already produces, so resolution treats templated and non-templated bases identically; a name with no template args passes through unchanged. Covers same-file and same-namespace bases (the dominant real-world patterns). A base in a different namespace referenced with its qualifier (`other_ns::Tpl<int>`) still doesn't resolve, but that's a pre-existing, orthogonal namespace-resolution gap — the non-templated `other_ns::Plain` fails identically — not a template issue. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cfa293539f |
fix(mcp): drain the event loop on Windows daemon shutdown instead of aborting mid-watcher-close (#1041)
On Windows, calling process.exit() while a recursive fs.watch handle is still tearing down aborts the daemon with a libuv UV_HANDLE_CLOSING assertion (0xC0000409) — reproducible whenever the indexed tree contains a nested repo (submodule / embedded clone), since that's what keeps a watch active at shutdown. A small exit delay doesn't help; only letting the loop drain is clean (verified on a real Windows VM: close()+exit() and close()+setTimeout(exit) both abort, while letting the loop drain exits 0). finalizeDaemonExit() now exits immediately on POSIX (unchanged) but on Windows marks success (exitCode=0) and lets the loop drain to a natural exit, with an unref'd backstop that force-exits only if a stray handle would otherwise hang shutdown. The daemon's own timers are already unref'd and its PPID watchdog lives in the proxy, so nothing keeps the loop alive past the closing watch handles — natural drain is fast. Pure + platform-injected so both branches unit-test off-Windows. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f73227d2f5 |
fix(mcp): don't warn "different git working tree" for submodules covered by the parent index (#1031, #1033) (#1039)
Indexing a super-repo now descends into its submodules and gitlinked clones, so a query run from inside one resolves up to the parent's unified index — whose graph DOES contain that nested repo's files. But the git-worktree-mismatch warning still fired, telling the agent the results were from "a different working tree" and to run `codegraph init -i` — which would split the submodule back into its own index and undo the unified view. A false positive carrying harmful advice. Distinguish a genuine borrowed worktree (the SAME repository on a different branch — shares a git common dir with the index root) from a submodule/embedded clone (a DIFFERENT repository — its own common dir), and suppress the warning only for the latter. Add gitCommonDir() for the check. The issue-#155 linked-worktree case is unchanged. Verified end-to-end: the warning no longer fires for a submodule-rooted MCP session and still fires for a real linked worktree. Edit-sync (manual sync + the live watcher) keeps the nested repo's files current on both macOS and Linux (active-submodule and bare-gitlink shapes), so suppressing the warning is safe. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a4dfc3438f |
fix(extraction): index nested repos recorded as gitlinks (#1031, #1033) (#1038)
A nested git repo tracked as a gitlink (mode 160000) — a clone `git add`ed into the super-repo without a `.gitmodules` entry, or a submodule that isn't active/initialized in this checkout — fell through both file-collection passes: it's tracked, so the untracked `-o` listing skips it, but it's not an active submodule, so `--recurse-submodules` won't expand it. Indexing the top level therefore pulled in only the outer repo's own files and stopped at the nested repo's boundary (one report: ~10 files at the root). Switch the tracked scan to `ls-files -s` to expose file modes, collect the unexpanded 160000 entries, and recurse into each that has a real working tree on disk as its own embedded repo. Mirror the same discovery in discoverEmbeddedRepoRoots so the watcher's scope stays equal to the indexer's. Active submodules (#147) and untracked nested clones (#193) are unchanged; gitlinks under default-ignored dirs (vendor/, node_modules/) stay excluded (#407); an uninitialized submodule with no checkout on disk is left alone. Adds four-shape coverage in extraction.test.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a79fa51816 |
feat(mcp): add readOnlyHint annotations so tools work in Cursor Ask mode (#1027)
All codegraph_* tools are query-only — they read the pre-built index and never mutate the workspace — but they advertised no MCP annotations, so Cursor's Ask mode (and any client that gates on read-only tools) blocked every call with "you are in ask mode and cannot run non read-only tools." Add a shared READ_ONLY_ANNOTATIONS constant (readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false) and reference it from each of the 8 tool definitions. The field flows through every tools/list path: the live getTools() (including explore's spread-rewritten description), the static proxy getStaticTools(), and the no-default withRequiredProjectPath schema clone. The annotations field is additive, so it ships without bumping the negotiated 2024-11-05 protocol version: clients that gate on it read it regardless, and older clients ignore it. Closes #1018 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9716fb27ae |
feat(extraction): parallelize indexing across a parse worker pool (#1015) (#1025)
indexAll parsed every file through a single worker thread, so a full `codegraph index` used one core no matter the machine. Add ParseWorkerPool (src/extraction/parse-pool.ts), modeled on the shipped QueryPool: indexAll now parses across clamp(cores-1,1,8) workers. CODEGRAPH_PARSE_WORKERS overrides the count; 1 reproduces the previous single-worker path exactly (the rollback). Parses run concurrently but results commit to SQLite in file order. This matters: the post-index resolution phase selects among ambiguous same-named candidates by node DB-insertion order, so a stable commit order keeps the graph deterministic — byte-identical to the serial path — instead of drifting with parse-completion timing. A bounded reorder buffer (backpressure on dispatched-but-uncommitted count) keeps memory flat even if a file is slow at the commit cursor. Crash/timeout of a worker rejects only that file's parse (feeding the existing retry pass) and respawns; per-worker recycle every 250 parses reclaims WASM heap. In-process fallback unchanged when the compiled worker is absent (tests). Validated on real OSS (django +9%, redis +17%; modest and parse-fraction-dependent), graph byte-identical across worker counts, peak memory flat-to-lower since workers recycle independently — so the #320 OOM concern doesn't materialize. Adds 11 pool unit tests. Closes #1015. Refs #320. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b3f59c717a |
fix(extraction): index Swift computed properties so they're findable (#1020) (#1024)
Swift in-class properties are extracted by a dedicated branch in TreeSitterExtractor.visitNode, not the generic nameField/variableTypes path swift.ts declares. That branch had a `!isComputed` gate that dropped computed properties entirely, so `codegraph query`/`codegraph_explore` returned "No results found" for them — including a SwiftUI view's `var body: some View`, the most important symbol in any SwiftUI app, and the heavily-read `var isCloudProxy: Bool` from the report. Stored properties were already fixed in #708 (v1.0.0); the reporter tested v0.9.9 and confirmed "still present on main" by inspecting swift.ts only, missing the dedicated branch — so only the computed-property half was real. - Computed properties now index as `property` nodes; the getter is walked via visitFunctionBody so its calls attribute to the property (a SwiftUI `body`'s subview tree becomes the property's callees — the render flow is traceable through it), not flattened onto the enclosing type. - Protocol property requirements (`var x: T { get }`) — a third never-indexed category — index as `property` too. - Routing the getter through visitFunctionBody also stops getter-local `let`/`var` declarations from being wrongly node-ified as struct fields (the generic child-walk used to do this): Alamofire property 0→348, field 618→588, idempotent. Stored/static behavior is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
30dc303f4c |
fix(db): chunk deleteResolvedReferences IN-list under the SQLite param limit (#1001) (#1023)
deleteResolvedReferences bound every id into a single unbounded `IN (...)`, so a list longer than SQLITE_MAX_VARIABLE_NUMBER (32766 on the bundled node:sqlite) threw "too many SQL variables" — the one IN-list in queries.ts that #540 missed. It's reachable only through the exported QueryBuilder (library use): the internal resolution path uses deleteSpecificResolvedReferences, which binds per-row and is immune, so the CLI/MCP indexing pipeline was never affected. Wrap it in the same SQLITE_PARAM_CHUNK_SIZE loop every sibling query uses, and add a regression test (33k ids, past the real 32766 ceiling) that throws without the fix. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f83a1ecc8e |
fix(mcp): start the daemon on ExFAT/FAT/network filesystems (#997) (#1022)
A project kept on an ExFAT/FAT external volume (or some network mounts / WSL2 DrvFs) broke the background auto-sync daemon at two points, both because the filesystem lacks POSIX features the daemon relied on: 1. Lock acquisition hard-links a temp file onto .codegraph/daemon.pid for race-free exclusivity (#411) — these filesystems have no hard links. 2. The Unix-domain socket listen() fails regardless of path length, so the old length-only tmpdir fallback never triggered. Both surface as a capability error, but each OS reports a DIFFERENT errno for the same gap (macOS ENOTSUP, Linux EPERM, Windows EISDIR), so the fix is policy-based rather than an enumerated code-set: - Lock: fall back to an O_EXCL create on any non-EEXIST link error. The temp write already proved the directory is writable, so the fallback either succeeds (still atomic + exclusive, "first writer wins") or surfaces its own genuine error. - Socket: an ordered candidate list [in-project, tmpdir] walked by BOTH the daemon (binds) and the proxy (connects) — they converge on the fallback with zero coordination. Relocate past any non-EADDRINUSE bind error; EADDRINUSE still rethrows, preserving the #974 contract. Normal repos are unaffected: the in-project candidate binds first, and the hard-link lock path is unchanged. Validated end-to-end on real removable-drive filesystems: macOS ExFAT (hdiutil image), Linux FAT32 (Docker loop mount), Windows exFAT (diskpart VHD) — each acquires the lock, relocates (or binds a named pipe on Windows), and serves a real client. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
45d3293c6a |
fix(resolution): stop "Resolving refs" wedge on theme-vendoring repos; add exclude config + index watchdogs (#999) (#1009)
Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under static/, ~1,600 tracked files): 1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped import-name collisions; this caps method-name collisions (init/update/render re-declared on every widget), which flow through matchMethodCall Strategy 3 and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline rather than score K candidates — no proximity score can pick the one true target among thousands anyway. Resolving drops from O(K^2) to linear in refs (e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap never fires on normal repos (max real method-collision ~40). 2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the existing includeIgnored plumbing across the git, sync, and non-git-walk paths. 3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which were serve-only), so a wedged or orphaned indexer self-terminates instead of pinning a core. The --liftoff-only relaunch's spawnSync can't forward signals, so killing the parent shim used to orphan the worker. Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git + non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d3179f5004 |
feat(mcp): require projectPath when the MCP server has no default project (#993) (#1007)
When the server runs with no default project to fall back to — a gateway server started outside any repo, or a monorepo root whose .codegraph/ indexes live only in sub-projects — every tool call must carry an explicit projectPath. Previously projectPath was always optional, so an agent talking to such a server would omit it, get success-shaped "pass projectPath" guidance, and not reliably retry; the user had to nudge it by hand. getTools() now marks projectPath required in the exposed tool schemas on the no-default-project branch (a high-salience channel clients surface/validate, unlike the instructions prose the reporter found too weak). When a default project is open, projectPath stays optional and a bare call falls back to it. The fix lives at the MCP schema layer, not the Claude-only front-load hook: the hook is local-filesystem-based and never runs for the reporter (they're on AGENTS.md / Codex-opencode). The proxy/getStaticTools path is untouched — index.ts forces direct mode whenever resolveDaemonRoot is null, so the no-default case never reaches the proxy. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b45f309a1b |
fix(prompt-hook): fire front-load hook for non-English prompts (#994) (#1004)
The UserPromptSubmit hook's structural-prompt gate was English-only, so a structural question written in Chinese — or any non-Latin script — silently injected nothing: JS `\b` is ASCII-only and never matches between Han characters, so the keyword regex couldn't fire (and couldn't be extended in place). To the user the hook looked unwired, with no error to explain why. Make the gate language-aware, split into tested helpers in directory.ts: - hasStructuralKeyword: English (\b-guarded) + CJK structural keywords. - extractCodeTokens: identifier-shaped tokens (camelCase / snake_case / name() / a.b) in any language — verified against the index via getNodesByName before firing, so a tech brand like `JavaScript` that looks like a symbol but isn't one here doesn't inject ~16KB of spurious context. - isStructuralPrompt: the cheap candidate gate (keyword OR code-token). Adds 21 unit tests for the gate (previously untested) covering the reporter's verification table plus the false-positive guards. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
703629edc3 |
feat(c/c++): resolve function-pointer command tables — macro-built, conditional-compilation & bare arrays (#991) (#1003)
* feat(c/c++): resolve macro-built function-pointer command tables (#991) C/C++ commands dispatched through macro-built function-pointer tables were dead-ends in the graph: redis' `call` never showed up as a caller of any command (`c->cmd->proc(c)`), because the table is generated into a #included `.def`, the handler is buried inside `MAKE_CMD(...)`, the struct type is itself a macro alias, the `proc` field uses a function-TYPE typedef, and the receiver is a chained field access. #954 deferred exactly this shape. Six composable additions to c-fnptr-synthesizer.ts close it: - function-type typedefs (`typedef RET T(...)` + `T *f`) flag the field as a function pointer; - multi-declarator fields (`struct redisCommand *cmd, *last`) each count as a slot/type (needed for positional alignment and the chain walk); - chained/array receivers (`c->cmd->proc`) resolve through field types across all same-named struct layouts (redis has two unrelated `client` structs); - `#include "x"` directives are followed (from raw source) so a non-indexed `.def` is read as a registration unit with the includer's effective macro env; - function-like + object-like macros are expanded (params->args, type aliases) before positional/designated registration; - a macro that expands to a brace-wrapped element (sqlite `FUNCTION(...)`) has one outer brace layer peeled. Validated on two independent macro-table lineages at 100% target precision: redis (209 commands via redisCommand.proc, `call`->every command) and sqlite (69 FuncDef.xSFunc targets). No regression on the controls: git (cmd_struct.fn, 138 builtins), curl (Curl_cftype.*), lua (0). 0 non-function targets across all five; +3 synthetic fixtures; full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(c/c++): resolve conditional-compilation command tables (vim) (#991) Vim's `:ex` and normal-mode command tables are the hardest fn-pointer-table shape: the struct is defined INLINE with the array, the whole thing is behind `#ifdef DO_DECLARE_EXCMD`/`DO_DECLARE_NVCMD` (switched on by the includer), built by a macro the file conditionally redefines (`EXCMD`/`NVCMD` = the table element under the switch, a bare enum id otherwise), and dispatched by a parenthesized array subscript through a file-scope table: `(cmdnames[i].cmd_func)(&ea)`. Four more composable additions on top of the macro-table work: - a focused `#ifdef`/`#ifndef`/`#if defined`/`#else`/`#elif`/`#endif` evaluator drops inactive arms (unevaluable `#if EXPR` keeps its body); an indexed header is re-scanned in an includer's context only when that includer #defines a switch the header guards, with the include's macros re-read from the resolved text (the plain last-wins parse picks the wrong, enum, arm); - inline `struct TAG {…} var[] = {…}` tables whose struct never became a node are parsed in place and registered; - array-subscript receivers (`tbl[i].f`) strip the subscript and resolve the base through a global-var → struct-type map; - an optional `)` before the call covers the parenthesized `(….f)(args)` form. Validated on vim: 273 `:ex` commands (`do_one_cmd`→every command) + 67 normal-mode commands, 0 non-function targets, 0 cross-table misroute (registering both tables is what stops `normal_cmd`'s `nv_cmds[i].cmd_func` from falling back to the `cmdname` owner of the shared field name). Controls unchanged at 0 non-function (redis/sqlite/git/curl gain coverage from array/global dispatch, lua still 0); +1 synthetic fixture; full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(c/c++): resolve bare arrays of function pointers (#991) The C/C++ fn-pointer synthesizer keyed everything on (struct type, fn-pointer field), so a dispatch through a bare array of function pointers — no struct, no field — was unbridged: an opcode/handler table like `static op_t *opcodes[256] = {nop,…}` invoked `opcodes[op](…)` left every handler with zero callers. Closes the last #991 deferred item. Keyed by the array VARIABLE name (a new `arrayReg`, parallel to the struct `reg`). Registration detects an array whose element type is a function typedef — a function-TYPE typedef element (`opcode_t *ops[]`, the `*` making it an array of pointers) or a function-pointer typedef element (`zend_rc_dtor_func_t t[]`) — and reads its literal entries, whether positional (`fn`/`&fn`), designated by index (`[IDX]=fn`), or cast-wrapped (`(cast)fn`). Dispatch is `tbl[i](…)` / `(*tbl[i])(…)`, gated on `tbl` being a known fn-pointer array (the precision anchor); the fan-out reaches the whole set (a runtime subscript hits any entry), like a command table. The same-file table wins on a name collision, so two file-local `static opcodes[256]` (SameBoy's CPU + disassembler) never cross. The fn-pointer typedef/field regexes now also tolerate a calling-convention macro before the `*` (`(ZEND_FASTCALL *name)`), which hardens the existing struct-field path too. Validated on two independent lineages: SameBoy (GB emulator) — 147 edges via `opcodes[]`, 0 cross-file leak; php-src (Zend) — 54 edges across 7 tables in the designated+cast+CC-typedef form. Control: lua 0 — its `lua_CFunction searchers[]` is pushed into the VM, never C-dispatched, so the call-gate fires nothing. No regression on the #991 corpus: redis (835) / sqlite (683) struct edges byte-identical, git +3 / curl +20 legitimate new bare-array edges, vim 433 with all guards holding; 0 non-function targets across all. + 4 fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfe13b03c8 |
feat(mcp): off-load read-tool dispatch to a worker pool to fix concurrent-call timeouts (#1002)
The shared daemon served every session on one event loop with synchronous node:sqlite. codegraph_explore is CPU-bound work stitched together by microtask awaits, so N concurrent explores keep the microtask queue continuously full and starve the macrotask phases — timers AND socket I/O. The transport freezes: no response can flush until the whole batch drains, so with ~10 subagents on a large repo clients routinely time out (reported via X by @symbolic2020). Move the heavy read-tool dispatch onto a worker-thread pool. Each worker holds its own WAL read connection (verified: a worker reader sees the main writer's committed catch-up/watcher writes); the single watcher/writer, the catch-up gate, codegraph_status, and the staleness/worktree notices stay on the main thread. Concurrent reads now run in true parallel up to core count and the main loop stays free for the MCP transport, so responses flush incrementally instead of all-at-once after the batch drains. Enabled for the shared daemon only; direct (single-stdio-client) mode is unchanged. - crash recovery: respawn + retry-once, with a circuit breaker that falls back to in-process dispatch if workers can't run on this platform - graceful backstop: an overloaded pool returns success-shaped "busy, retry" guidance, never isError (so it can't teach the agent to abandon codegraph) - pending-aware growth + capped concurrent cold-starts avoid a startup thundering herd (N simultaneous module-loads + DB opens could stall the loop) - config: CODEGRAPH_QUERY_POOL_SIZE (default clamp(cores-1, 1, 16); 0 disables → in-process), CODEGRAPH_QUERY_BUSY_TIMEOUT_MS (default 45s) 10 concurrent explores on vscode (10.5k files): 31s → ~9s, staggered flush, 0 timeouts, byte-identical output; scales with cores (≈3.3× on 8, 1.8× on 2). Full suite passes plus 10 new query-pool tests (fake-worker injection so the scheduling logic is covered without spawning threads). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7c6417ef8f |
fix(mcp): prevent "Transport closed" from a stray daemon-socket error (#974) (#983)
The client-facing MCP proxy could exit with "Transport closed" when its connection to the shared daemon hit a socket 'error' with no listener attached — common on WSL2 /mnt (DrvFs), where AF_UNIX is flaky. The global fatal handler turned that uncaughtException into process.exit(1), which the MCP client saw as a bare transport close even though the index was healthy. proxy.ts now keeps an 'error' listener on the daemon socket for its whole life (and skips a socket destroyed in the connect window), so a stray error degrades to the existing in-process fallback instead of crashing. daemon.ts releases the lockfile it acquired when it fails to bind, so the next launch doesn't spin on a stale lock (the duplicate serve --mcp pileup). No default behavior change for anyone; WSL /mnt users who still hit trouble can set CODEGRAPH_NO_DAEMON=1 to skip the shared daemon entirely. Validated on macOS (unit + live serve probe) and Linux (Docker, --init): 64/64 across the daemon/socket/lifecycle suites, incl. real AF_UNIX. Closes #974 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
73bcc1afb4 |
fix(extraction): respect .gitignore by default for embedded-repo discovery (#970, #976) (#980)
#514 (v1.0.0) began walking into gitignored directories to discover and index the git repos nested inside them. That broke users who rely on .gitignore to exclude a directory: a gitignored folder of cloned reference repos blew graphs up (one report went 10k to 500k edges, #976) and stalled indexing on multi-gigabyte trees of clones (#970). Respect .gitignore by default again. Discovering embedded repos inside a gitignored directory is now opt-in via codegraph.json: { "includeIgnored": ["packages/", "services/"] } The single choke point findIgnoredEmbeddedRepos now returns nothing unless a gitignored dir matches the project's includeIgnored patterns, and the matcher is threaded from the scan root through the full-index, incremental-sync, and watcher-scope paths. Downstream ScopeIgnore and the watcher are unchanged: they key off the discovered embedded roots, so gating discovery fixes the indexer, sync, and watcher together. Untracked embedded repos (#193) stay indexed by default. This restores the super-repo-of-clones behavior (#622, #699) for the people who want it, while making the default match what every other tool (and CodeGraph's own git ls-files foundation) does: .gitignore excludes. project-config.ts now parses codegraph.json once (loadParsedConfig) and exposes loadIncludeIgnoredPatterns alongside the existing extension map. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
85a8f32fd9 |
fix(mcp): serve tools without a root index + make the front-load hook monorepo-aware (#964) (#966)
The MCP server gated tool availability on whether the server root had a .codegraph/ index, so in a monorepo where only sub-projects are indexed the agent saw zero tools — and couldn't reach an indexed sub-project even by projectPath. A session started before `codegraph init` also never surfaced the tools afterward. The Claude front-load hook had the mirror gap: it only walked UP for an index, so it stayed silent at a monorepo root. MCP server: - Always expose the tool surface; when the root isn't indexed, send a per-project instructions variant (pass projectPath) instead of the "inactive" note. Safety comes from response SHAPE (success-shaped guidance, never isError), not from hiding tools. - Reword the no-default-project guidance to be per-project, not per-session, and sharpen the projectPath schema description. Front-load hook (UserPromptSubmit): - Scan DOWN (bounded depth, workspace-root-gated) for indexed sub-projects and shape the injection by topology: front-load the one the prompt names, nudge about the rest, or list them when ambiguous. Verified: full suite (1703 passed); a live two-package monorepo run confirms the hook front-loads the correct sub-project with no cross-package leakage. The front-load's net speed effect is the existing multi-file-vs-single-file tradeoff, unchanged by this work. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0a91d0f512 |
perf(resolution): fix O(K²) import-node blowup in "Resolving refs" (#915) (#965)
* perf(resolution): resolve imports to definitions, not sibling import nodes (#915) "Resolving refs" crawled (tens of minutes) on large projects — most painfully ones mixing a big front-end and back-end. An external package or module imported across hundreds/thousands of files (react, a shared UI package, Python logging/typing) is re-declared as an `import` node in every importing file, so its unresolved import ref fell through to the exact-name matcher, which scored all K same-named import nodes via findBestMatch — K refs x K candidates = O(K^2) per package, producing only meaningless import->import edges. Fix: exclude `import`-kind nodes as name-match targets (they're statements, not definitions; real import->definition resolution is the import resolver's job). Plus two safe constant-factor wins in findBestMatch: hoist the per-candidate ref.filePath split, and skip cross-language candidates when a same-language one exists (provably the same winner — same-language scores >=50, cross-language maxes at 35). Measured: superset (Py+TS) candidates scored 7.5M -> 833K (9x), non-import edges preserved (+1618 now resolve to real defs), ~22K useless import->import edges removed; kubernetes (Go) computePathProximity 37.2s -> 5.0s; synthetic 8k-file mixed repo (K=4000) resolution 16.0s -> 1.7s. Full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: correct stale better-sqlite3/wasm references to node:sqlite The SQLite backend has been Node's built-in node:sqlite (real SQLite, WAL + FTS5, from the bundled runtime) for a while — there is no native build step and no node-sqlite3-wasm fallback. README and the docs site were already updated; this catches the stragglers: - CLAUDE.md: the src/db/ backend description and the sqlite-backend test note. - src/db/index.ts, src/mcp/tools.ts: two code comments that still blamed "the wasm backend" for non-WAL behavior (reworded to "when WAL isn't in effect"). Leaves tree-sitter grammar wasm (web-tree-sitter / --liftoff-only) untouched — that's a different, still-current use of wasm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(telemetry): drop the dead sqlite_backend field (schema v2) node:sqlite is now the only backend, so the `index` event's `sqlite_backend` field was a constant ("native") carrying no signal — and the `install` event never actually sent it. Remove the field and the backendKind() helper, bump the telemetry SCHEMA_VERSION 1 -> 2, and update TELEMETRY.md + docs/design/telemetry.md. The ingest worker is deliberately left tolerant: `index` doesn't require the field and schema_version validates as nonNegInt(99), so v2 events ingest fine and old clients still sending v1 + sqlite_backend keep validating too. Added a legacy comment there explaining it's safe to drop once old-client share is negligible. telemetry.test.ts: the assertion pinning schema_version and a stale-claim fixture line updated 1 -> 2. All telemetry tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a89315645d |
feat(go): index GoFrame g.Meta routes and bind them to controller methods (#747) (#957)
GoFrame's standard router binds routes reflectively (group.Bind(ctrl)): the path and method live in a g.Meta struct tag on a request type, and the controller method that serves it is matched by that request type at runtime — so there was no path string and no edge from a route to its handler, and "where is this route handled / where are routes bound to controllers?" could only be answered lexically (issue #720's report). - frameworks/goframe.ts: detect gogf/gf in go.mod, extract each path-bearing g.Meta into a route node (requires path:, so response mime:-only tags are skipped), encoding the package-qualified request type for the join. - goframe-synthesizer.ts: join each route -> the controller method whose signature takes that request type — NOT by name (DeptSearchReq is served by List) — keyed pkg.Type to disambiguate the many identical bare names a large app defines one-per-module, with an addon-root tiebreak for cloned demo addons. Edge kind calls, provenance heuristic, synthesizedBy goframe-route, surfaced as a dynamic-dispatch hop in codegraph_explore. Validated on real repos: gf-demo-user 7/7, gfast 65/68 (3 genuinely handler-less), hotgo 242/247 (98%) — 100% precision (0 non-controller handlers, 0 core/addon cross-binding), node count stable. Agent A/B (gfast, sonnet/high, 2 runs/arm): with codegraph 1 explore call / 0 Read / ~20s vs without 7.5 Read avg + grep-hunting for the non-existent literal route string / ~42s; same correct answer. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6459ead6aa |
fix(extraction): index files reached through in-root symlinks that point outside the repo (#935) (#956)
The directory walk deliberately follows an in-root symlink whose target lives outside the repo root (the standard Dota custom-game layout, where `game/` and `content/` link into the SDK tree) and enumerates the files under it. But the read path then rejected every one of them via the strict symlink-escape guard, logging `Path traversal blocked in batch reader` and indexing nothing — discovery and the reader disagreed. Add an opt-in `allowSymlinkEscape` to validatePathWithinRoot that waives only the realpath-escape rejection (the lexical `../` guard still applies) and pass it at the three indexing read sites (batch reader, indexFile, indexFileWithContent). The content-serving sinks (ContextBuilder, MCP tools) keep the strict guard, so this stays inside the #527 model: indexing now follows the symlink, getCode still refuses to serve out-of-root contents. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d1121e46f0 |
feat(config): map custom file extensions to languages via codegraph.json (#906) (#955)
The extension → language table was hardcoded, so a codebase using a
non-standard extension for a supported language (e.g. `.dota_lua` for Lua)
had those files silently skipped — no way to opt them in short of patching
the source.
Add an opt-in, project-scoped `codegraph.json` at the repo root:
{ "extensions": { ".dota_lua": "lua", ".tpl": "php" } }
Mappings merge on top of the built-in defaults and take precedence (so a
built-in can be re-pointed, e.g. `.h` → `cpp`). Absent or malformed config
is the zero-config default — byte-identical to prior behavior; an invalid
target language or unparseable file is warned-and-skipped, never fatal.
Implementation:
- New `src/project-config.ts` — `loadExtensionOverrides(rootDir)`, validated
against `isLanguageSupported`, mtime-cached per root.
- `detectLanguage` / `isSourceFile` gain an optional `overrides` arg
(omitting it is the existing behavior).
- Overrides threaded per-operation through every extraction call site
(scan/walk gates, git change-detection, grammar selection, extraction,
the file watcher), resolved from the project root — no process-global
state, so the multi-project daemon stays isolated. The parse worker
receives the resolved language in its message.
Tests: 13 new cases (unit, loader validation/normalization/caching, and a
full-index integration proving a custom-extension file is extracted while
the zero-config path indexes nothing). Worker path smoke-tested via the
built CLI.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ba209d9489 |
feat(c/c++): resolve function-pointer dispatch (#932) (#954)
C/C++ polymorphism is the function pointer: a struct fn-pointer field, concrete
functions registered into it through a table (`{"add", cmd_add}`), a designated
initializer (`.handler = on_open`), or an assignment, then dispatched indirectly
(`p->fn(argv)`). Static extraction captures neither the registration→field
binding nor the indirect call, so the dispatcher→handler edge was missing — git's
run_builtin looked like it called nothing, a vtable's implementations had no
callers, and the hook_demo.c in the issue was unreachable.
Add a resolution-layer synthesizer keyed by (struct type, fn-pointer field). It
reads source (the established Celery/Sidekiq/Spring pattern — C extraction has no
struct fields or indirect-call edges to build on) in passes: collect fn-pointer
typedefs, parse struct field layouts, collect registrations (positional matched
by field index, designated, and assignment), propagate field←field assignments
(so a generic hook slot reassigned from a registry — the hook_demo.c
`h->func = found->fn` shape — inherits the registry field's handlers), then link
each indirect dispatch site to the registered handlers. Receiver type resolves
from the enclosing function's params/locals, falling back to a field name unique
to one struct. Covers both the command-table idiom (git, redis) and the
ops-struct/vtable idiom (curl content-encoders, protocol handlers).
Pure edge synthesis (no node growth); high precision via the (struct, field) key.
Validated: git 502 edges (run_builtin→cmd_* plus git_hash_algo/archiver/reftable
vtables), redis 357 (dictType.hashFunction, connection + reply-object vtables),
curl 478 (Curl_cwtype.do_init → deflate/gzip/brotli/zstd); 0 non-function targets
on all three; node-stable; 0 on the lua control (its {name,fn} tables register
into the Lua VM, with no C indirect call to bridge). Full suite 1665 pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
826810f128 |
feat(java): index Lombok-generated members so call chains resolve (#912) (#953)
Lombok generates getters/setters, builder(), equals/hashCode/toString, and the @Slf4j log field at compile time, so they never appear in the source AST. Static extraction missed them entirely, so a bean.getName() / User.builder() / log.info() call resolved to nothing and call-chain analysis broke silently — the agent would conclude the method didn't exist. Add a synthesizeMembers hook on LanguageExtractor, called at the end of class extraction (class still on the scope stack, real members already extracted), and a Java implementation that synthesizes the mechanical members for @Getter, @Setter, @Data, @Value, @Builder/@SuperBuilder, @ToString, @EqualsAndHashCode, and the @Log* family. Each node is anchored on the field/class name-token leaf (so it pulls in no spurious value-reference scope), marked with a `lombok` decorator and a docstring naming the generating annotation, and never overrides a member the source already declares. Methods and fields are deduped separately since they're distinct namespaces in Java (a boolean field `isRunning` and its generated getter `isRunning()` coexist). Deliberately not synthesized: constructors (new X() already links via instantiates, and overloaded @NoArgs/@AllArgs/@RequiredArgs ctors would collide on a synthetic node id), fluent builder setters, and @Accessors(fluent=true). Validated on eladmin (274 Java files, Lombok-heavy): 100% accessor precision (878/878 map to a real field), 722 previously-broken calls now resolve; spring-petclinic (no Lombok) control synthesizes nothing. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |