[0.9.6] - 2026-05-27
- C/C++
#includeresolution — bare-basename includes now connect to the actual header file, not a phantom import node (#453). Path-prefixed includes (#include "common/args.h") already resolved via file-path suffix matching, but bare-basename includes (#include "uint256.h"from a caller in another directory) used to leave only a phantom edge to a floatingimportnode owned by the including file. The resolver now walks C/C++ include search directories — pulled fromcompile_commands.json(-I/-isystemflags) when present, otherwise discovered by probing conventional dirs (include/,src/,lib/,api/,inc/) plus any top-level directory containing.h/.hppfiles — and resolves the include to a real file node, producing a true file→fileimportsedge. System headers (<stdio.h>,<vector>,<iostream>, ~80 C and ~80 C++ stdlib names) are filtered before the scan so they don't false-resolve via heuristic dir matching. C/C++ built-in symbols (std::*unconditionally, plusprintf/malloc/cout/make_shared/etc. when no user-defined symbol with that name exists) are filtered from name-matching too — C/C++ projects routinely shadow stdlib names (custom allocators, stream wrappers, logging libs), so the filter only fires when there's no real definition to bind to. Measured on bitcoin-core (1,989 indexed files): C/C++ file→fileimportsedges 6,027 → 8,086 (+34%), false-positive call edges fromstd::move/std::swapetc. into similarly-named user methods −2,154 (−3.6% of C/C++calls). - Enterprise Spring / MyBatis flow now traces end-to-end (#389). Three gaps that previously forced agents back to grep on large Spring/MyBatis codebases are closed:
- MyBatis XML mapper indexing + Java↔XML bridge.
*.xmlfiles containing<mapper namespace="...">are now first-class: each<select|insert|update|delete id="X">and<sql id="X">becomes a method-shaped node qualified as<namespace>::<id>, and a new synthesizer (mybatis-java-xml) links the matching Java mapper interface method → its XML statement with acallsedge.<include refid="...">to a<sql>fragment in the same mapper also resolves. Non-mapper XML (pom.xml,web.xml,log4j.xml, etc.) emits just a file node — no symbol noise. Validated on macrozheng/mall-tiny: all 6 custom-SQL Java mapper methods reach their XML counterparts;trace(UmsRoleController.listResource, UmsResourceMapper::getResourceListByRoleId-xml)connects in 4 hops across controller → service-iface → impl → mapper-iface → XML. - Spring
@Value/@ConfigurationPropertiesconfig-key linkage.application.{yml,yaml,properties}(+ profile variantsapplication-dev.yml,bootstrap.yml, etc.) is parsed during indexing, with oneconstantnode per leaf key qualified by its dotted path (app.cache.name.user-token).@Value("${app.cache.name.user-token}")and@ConfigurationProperties(prefix = "app.cache")references in Java/Kotlin emit binding nodes that resolve to the matching key (or, for@ConfigurationProperties, a key under the prefix). Spring's relaxed binding applies (kebabcache-list↔ camelcacheList↔ snakecache_list↔CACHE_LIST), so a Java@Value("${app.retryCount}")findsapp.retry-countinapplication.properties.${key:default}form is supported; the default is stripped before lookup. - Field-injected concrete-bean trace. A Spring controller's
@Resource(name="userBO") private UserBO userbo;followed bythis.userbo.toLogin2(...)now resolves through toUserBO.toLogin2even when the field type is a concrete class whose name doesn't match the field by Java naming convention (userbo→UserBO). The fix is two layered changes in the language layer (Java only): (a) the call extractor unwrapsthis.<field>receivers (previously surfaced asthis.userbo.toLogin2and dropped through every name-matcher strategy); (b) the resolver looks up the receiver name in the enclosing class's field declarations and uses the declared type to resolve the method. This generalizes beyond Spring — any Java code usingthis.field.method()now resolves correctly.
- MyBatis XML mapper indexing + Java↔XML bridge.
Fixed
- Java/Kotlin imports now disambiguate same-name classes across modules (#314). A Maven multi-module project where
dao/converter/FooConverterandservice/converter/FooConverterboth expose aconvertmethod used to resolve via file-path proximity — picking whichever class was closer to the caller, which is wrong any time the caller lives in an equidistant cross-cutting module. The import resolver had no Java branch at all (extractImportMappingsreturned[]for.java/.kt), so the FQN signal Java imports carry —import com.example.dao.converter.FooConverter;— was being thrown away. NewextractJavaImportsparses regular andimport staticdirectives.resolveViaImportnow has a Java/Kotlin cross-file branch that converts the imported FQN to a file-path suffix (com/example/dao/converter/FooConverter.java) and resolves the symbol against the file whose path matches. For the@Autowired private FooConverter fooConverter; fooConverter.convert(...)field-receiver pattern (Spring's typical shape),matchMethodCallnow passes the imported FQN toresolveMethodOnTypeso when multipleFooConverter::convertcandidates exist, the import — not iteration order — picks the right one. Validated end-to-end on a synthetic two-module repro: swapping only theimportline on the caller (with identical field declaration and call site) switches the resolved target between dao and service correctly. On spring-petclinic, +15 newly import-resolved Java edges with no regression incalls/imports/extends. - TypeScript
typealiases with object shapes no longer cause cross-module false-positive call edges (#359). Receiver-typedhandle.stop()wherehandle: RecorderHandleandRecorderHandle = { stop: () => Promise<void> }used to attach the call edge to an unrelatedclass Foo { stop() {} }in a sibling directory via path-proximity matching, because the type alias had nostopnode — only the look-alike class did. The fix surfaces type-alias object-shape members (and intersection-type members) as first-classproperty/methodnodes under the alias:type X = { foo: T; bar(): T }now producesX::fooandX::barin the graph. Function-typed properties (stop: () => Promise<void>) are emitted asmethodkind soobj.stop()resolves to them; non-function properties remainpropertykind. With the alias's members in the graph, the existing camelCase receiver-name word overlap (recorder↔RecorderHandle) routes the call to the correct alias member instead of the wrong class. Anonymous nested object types inside generic arguments (Promise<{ ok: true }>) intentionally don't produce phantom members — only immediateobject_type/intersection_typeoperands of the alias value are walked. Measured on excalidraw/excalidraw (314 .ts files): +776 new property nodes + +1,008 method nodes from type-alias members + +226 newly accuratecallsedges pointing at alias members (some shifted from incorrect class targets, some previously unresolved). - C# now produces
referencesedges for parameter, return, property, and field types (#381). Indexing any C# project used to yield zeroreferencesedges, socodegraph_callers SomeDtoreturned no results even when the DTO was used as a parameter or return type across the codebase, andcodegraph_calleeson a service class only saw itsusingimports. Two root causes:csharp.tswas missingreturnField, and the type-leaf walker only matchedtype_identifiernodes — but C# tree-sitter emitsidentifier/predefined_type/qualified_name/generic_nameinstead. The fix adds the missing extractor field, routes C# through a dedicated type walker that only descends into known type-position fields (so parameter NAMES likerequestinBuild(UserDto request)never mis-emit as type refs), and hooksextractField/extractPropertyto invoke the walker. Measured on dotnet/eShop (527.csfiles): C#referencesedges go from 35 → 925 (+26x), with no regression incalls/imports/instantiates/extends/implements. - Go cross-package qualified calls (
pkga.FuncX(...)) now resolve to the right package (#388). On a Go monorepo with a layered package layout (handler/service/domain/dao),codegraph_callers,_callees,_impact, and_traceused to return ~0-1 results where grep finds hundreds to thousands of real call sites — the central value proposition of CodeGraph silently degraded on entire Go codebases. Root cause: the import resolver flagged every Go import path without/internal/as third-party (because it had no idea what the project's own module path was), so cross-package calls fell through to name-matching with path-proximity scoring, which on real codebases picks ~one accidental candidate per call site. The Go branch now reads the project'sgo.mod, treats<module-path>/...imports as in-module, and looks up the qualified symbol in the imported package's directory; same-name functions in different packages no longer collide. As a side fix, Go nodes now correctly carryis_exported=1for capitalized identifiers (the resolver needs this to filter candidates). Measured on gRPC-Go (1,031.gofiles, layered packages): cross-packagecallsedges go from 10,880 → 19,929 (+83%), totalcallsfrom 23,803 → 34,105 (+43%), with no false-positive resolution of stdlib calls (fmt.Printlnetc. stay external). codegraph_filesnow returns the whole project when an agent passespath="/",".","./","", or a Windows-style"\\"— instead of "No files found matching the criteria." Indexed file paths are stored as project-relative POSIX (e.g.src/foo.ts), but the path filter used a plainstartsWith, so a leading slash or any of the other root-ish shapes an agent might guess matched nothing and pushed the agent back to Read/Glob — the exact opencode + Gemini Flash regression reported on Windows 11. Subdirectory filters are now equally forgiving:"/src","./src","src/","src\\components", etc. all resolve correctly. Sibling-prefix bleed ("src"was previously matchingsrc-utils/...) is also fixed — the filter now requires either an exact match or a<filter>/boundary. Closes #426.- File watcher no longer marks edited files as fresh when another process holds the index lock. When a second writer (concurrent
codegraph index, a git hook, another MCP daemon) held.codegraph/codegraph.lock,CodeGraph.sync()returned a zero-shape no-op instead of throwing. The file watcher took that as a successful sync and clearedpendingFiles— so the per-file staleness signal MCP tools surface to agents (issue #403) dropped immediately, even though the edit was never indexed.CodeGraph.watch()now converts that no-op into a typedLockUnavailableErrorthrown into the watcher; the existing retry path preservespendingFilesand reschedules until the lock becomes available. The error is logged at debug only (noonSyncErrorcallback) so a long-running external indexer doesn't spam stderr every debounce cycle. Closes #449. - TS/JS top-level initializer calls and inline-object-method calls are no longer dropped. Calls inside a top-level variable initializer (
const token = getTokenMp()) and inside methods of an inline object literal ({ methods: { save() { getTokenMp() } } }) were never walked by the variable / method-definition extractors, sogetTokenMpshowed up nowhere incodegraph_callers. The variable extractor now walks any non-object initializer value for calls; the method-definition extractor still avoids creating synthetic nodes for inline-object methods (the noise reason is unchanged) but now walks their bodies so the calls inside aren't lost. Surfaces in plain.ts/.jsfiles (top-levelconst x = foo()) and in Vue SFCs (<script setup>initializers + classic Options APImethods: {...}/setup()), where the bug was originally reported. Closes #425. - Watch sync no longer aborts with
FOREIGN KEY constraint failed. PR #62 plugged this FK violation at the extraction layer (empty-named nodes whose containment edges had no target), but the same violation kept reappearing on v0.9.5 during the daemon's watch sync — not on initial index. Once an agent's daemon had been running long enough to accumulate edits, a resolver lookup that crossed a framework-specific cache could hand back a node whose row had been removed by a recent file rewrite, and the FK check then aborted the entire resolution batch, leaving the user's daemon log filling withWatch sync failed { error: 'FOREIGN KEY constraint failed' }.QueryBuilder.insertEdgesnow validates every batch's endpoints against thenodestable directly (one freshSELECT id IN (...)per batch, no cache) and silently skips edges with missing source or target — so a stale lookup result drops one edge instead of aborting the whole sync. Surfaces as a freshcodegraph init/indexcycle now surviving its first watch-sync cycle without the FK error, and the daemon recovering naturally instead of compounding into further failures. Closes #455. - Hermes Agent:
codegraph install --target hermesno longer corrupts~/.hermes/config.yaml. Hermes serializes its config with PyYAML's default block style, which writes list items at the same indent as the parent mapping key (cli:and- hermes-cliboth at column 2). The previous line-based YAML patcher mistook that first- hermes-clifor the next sibling key, truncated thecli:block, and then spliced- mcp-codegraphat indent 4 before the existing items — leaving subsequent entries (- browser,- clarify, …) and even other platforms (telegram:,discord:) appearing at theplatform_toolsets:level, which is no longer parseable YAML. The installer now recognizes the same-indent list style, finds the real end of the block at the next sibling key, and appends- mcp-codegraphat whatever indent the existing items already use. Re-installing on an already-corrupted file (or a 4-space-nested config that worked before) still produces a clean, parseable result. Closes #456. - NestJS:
RouterModule.register([...])route prefixes now propagate to controller routes. Previously a controller declared inside a module wired through NestJS'sRouterModule(a common pattern for modular apps with nested route prefixes) was indexed with its raw@Controller(...) + @Get(...)path — soUsersControllerunderRouterModule.register([{ path: 'admin', module: AdminModule, children: [{ path: 'users', module: UsersModule }] }])showed up asGET /instead ofGET /admin/users. The new cross-file pass walks every*.module.{ts,js}forRouterModule.register/forRoot/forChild([...])(recursivechildren) and@Module({ controllers: [...] }), then prepends the correct prefix to each affected route — including non-empty@Controllerpaths and method-level params (/admin/users/:id). The route node'sidis preserved across the update so existing route→handler edges stay intact, and the pass is idempotent so incremental sync recovers whenapp.module.tsitself is edited. Closes #459. - C++ callers now resolve through typed member pointers (#445/#454).
codegraph callers CDetect::Processingpreviously returned nothing for call sites likem_cpAlg->Processing()because (a)field_expressioncalls weren't kept as receiver-qualified references, (b) out-of-line method definitions (int CDetect::Processing() {...}in.cppwhile the class is in.hpp— typical C++ layout) weren't surfaced as class methods, and (c) the resolver had no way to bridge aType* receiverdeclaration to its method calls. Calls through typed object pointers, references, and stack values now resolve to the right class's method, including the common ambiguous-name case (two classes with a shared method name) and call sites embedded inreturn ptr->m()orType x = ptr->m()forms. Measured on bitcoin-core (1306 .cpp files): +6.1% C++ caller edges resolved end-to-end.
Added
- Installer targets for Gemini CLI and the Antigravity IDE.
codegraph install(and the interactive prompt) now detect and configure two more agents out of the box:- Gemini CLI (also covers the rebranded Antigravity CLI) — writes
mcpServers.codegraphto~/.gemini/settings.json(global) or./.gemini/settings.json(local), and the codegraph usage block into~/.gemini/GEMINI.md/./GEMINI.md. Existing top-level settings (e.g.security.auth) and sibling MCP servers are preserved. - Antigravity IDE — writes
mcpServers.codegraphto Antigravity's unified MCP config at~/.gemini/config/mcp_config.json(post-migration, signalled by the.migratedmarker Antigravity itself drops). Falls back to the legacy~/.gemini/antigravity/mcp_config.jsonfor users on a pre-migration Antigravity build. On install, a stale codegraph entry in the legacy path is migrated to the new file automatically. Uninstall sweeps both. Antigravity-managed sibling fields (e.g. the"disabled": trueflag the IDE adds when a user disables a server through the UI) are preserved across re-installs. - The Antigravity entry omits the
type: "stdio"field the other targets use — Antigravity rejects entries that carry it. - On macOS, the Antigravity entry resolves
codegraphto its absolute path at install time (e.g. an nvm-managed~/.nvm/.../bin/codegraph). macOS GUI apps launched from Dock/Finder get a stripped PATH that doesn't include nvm, so a bare command name fails to spawn — even whenwhich codegraphworks in your shell. Linux GUI apps inherit user PATH and Windows uses envPATHdirectly, so both keep the bare command. - The IDE shares
GEMINI.mdwith Gemini CLI, so the two targets compose naturally when both are installed; the antigravity target deliberately doesn't touchGEMINI.mdso uninstalling Antigravity alone leaves CLI instructions intact.
- Gemini CLI (also covers the rebranded Antigravity CLI) — writes
Both targets are tested on the same parameterized contract as the existing five agents (idempotent install, sibling preservation, install/uninstall round-trip), with extra coverage for migration-marker detection, legacy → unified entry migration, sibling disabled field preservation, and the cross-target case where Gemini CLI and Antigravity IDE coexist in the same ~/.gemini/. Closes #399.
- Installer target for Kiro (CLI + IDE).
codegraph installnow detects and configures Kiro out of the box on macOS, Linux, and Windows. WritesmcpServers.codegraphto~/.kiro/settings/mcp.json(global) or./.kiro/settings/mcp.json(local), and the codegraph usage block into a dedicated~/.kiro/steering/codegraph.md/./.kiro/steering/codegraph.md— Kiro's steering system loads every*.mdfile insteering/as agent context, so a dedicated file is the natural surface (no marker-based merging required). Sibling MCP servers inmcp.jsonand unrelated steering files (product.md,tech.md, etc.) are preserved across install / uninstall. Tested on the same parameterized contract as the other agent targets (idempotent install, sibling preservation, install/uninstall round-trip). Closes #385.