ecd6e1cd15debf0c49b93ffd4ed079b2391af01a
72
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ecd6e1cd15 |
feat(ui): live refresh and drift banners — the viewer keeps up with the project (CG-53)
`GET /api/events` is a server-sent-event stream the viewer holds open for the life of the page. Two signals, two things the browser could not know: changed source files touched on disk, before any sync — the drift banner index the graph moved, naming what the sync re-indexed — the live refresh The server WATCHES and never syncs: the project tree through the engine's own FileWatcher with a notify-only syncFn, the index through one non-recursive fs.watch on the data directory settled at 400 ms. Both start with the first subscriber and stop with the last, so a viewer nobody has open costs no watch descriptors. Nothing polls, on either side. Drift is now parity with codegraph_node (#1474) rather than an absence. `/api/source?ondrift=current` serves a drifted file's CURRENT bytes flagged `showing: 'current'`, and the three screens that can say so switch off everything anchored to the old line numbering — gutter ports, call-site links, call arcs, the callee rail's anchoring — while keeping the source. The banner is paper-2 with a hairline rule, never amber: amber belongs to the untested badge. Also fixes a stale read this exposed. A long-lived reader holds an LRU of nodes by id that only its own writes invalidate, so `/api/node/<id>` kept answering with a symbol another process's sync had deleted while `/api/search` beside it said it was gone. GraphSession now drops the read caches when the database (or its WAL) has been written, and the Symbol view follows a symbol whose id changed because an edit above it moved its start line, carrying the trail across. Measured on a live viewer: banner 360 ms after a save, toast 440 ms after `codegraph sync` returns, 0 requests in 4 idle seconds, and the client gives up reconnecting after ~90 s with "Not live" rather than hammering a dead port. |
||
|
|
bd99c5e99a |
feat(ui): the whole file — full source with gutter ports and intra-file call arcs (CG-52)
The File view gains a Source tab: the file itself, top to bottom, with the Symbol view's line grid, gutter ports and call-site links, a line-anchored callee rail, and — in the left margin — an arc for every call that stays inside the file, drawn from the calling line to the callee's definition line. The arcs are the point. Source order is already a layout, chosen by whoever wrote the file, so a file's internal call structure can be drawn with no algorithm placing anything. Crabviz's idea, in the one place it is legible. Everything is arithmetic, not measurement. The Symbol view queries the laid-out DOM to place a callee row beside its line; a 6 820-line file cannot afford that. Here a line is exactly 20px at `10 + (n - 1) x 20`, so ~90 line elements exist at a time and the arcs, ports, rail rows and connectors are all functions of a line number. `src/mcp/tools.ts` scrolls at a 16.6ms median frame. - `GET /api/filecode/<path>` — outline, one call group per (caller, callee) PAIR with its call-site lines, unresolved references, and the file's length. The source is NOT in it: it pages through `/api/source` 800 lines at a time with a discarded 150-line lead-in, so a page starting inside a block comment does not render prose as code, and so the ports and arcs are complete from the first frame while the text fills in behind them. - `intraFileCalls` is counted over the groups actually returned, so the header and the picture under it cannot disagree once a cap bites. - Above 40 arcs the diagram narrows to the symbol under the pointer (or the one the scroll position is inside) and the header states the total. Accent is for the pointer only, never for the filter. - Sticky outline rail at >= 1400px, following the reader down the file. - `QueryBuilder.getUnresolvedReferencesInFile` — one indexed lookup instead of one per symbol; `buildOutlineEntries` lifted out of `/api/file` so both readings of a file draw the same rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6d0f60f32c |
feat(ui): the Map — the repository at module granularity, layered from the graph (CG-49)
`GET /api/map` rolls the whole edge table up to module granularity in one `GROUP BY`, and the Map tab draws it: one box per directory, dependencies pointing down, nothing placed by hand. Two decisions carry the screen. The vertical order rests on each link's `declared` weight — the edges resolved through an import, a qualified name, an inheritance clause or a typed receiver — not on its raw count. Bare name matching resolves `run`, `push` and `finish` across unrelated directories, and layering on raw counts put `src/db` directly under `src/bin` on this repository's own index. On declared edges the same data reproduces the pipeline CLAUDE.md describes, with a third of the mutual pairs. When too few links carry a declared edge to describe a project, the layout falls back to raw counts and the side panel says so. And the aggregation is a single scan. Grouping by the symbol names as well as the modules costs nothing extra — the join is what is expensive — so one query yields both the link weights and the tooltip's symbol pairs. Measured against this index inflated to 800k edges: 1.28s for one scan against 1.89s for two, which is the difference between meeting and missing the cold budget on a ten-thousand-file repository. Cached answers come back in ~3ms. Nothing is dropped silently: thin links are hidden until a module they touch is selected and counted in the panel, uncertain references are excluded from every number on screen and the total is printed, and mutual dependencies, module loops and file-level circular imports are listed rather than straightened away. An edge that still points up after layering is drawn dashed on selection instead of being reversed or removed. The layout — cycle-breaking, longest-path layering, barycenter ordering, ports — is a pure function of the payload in `ui/src/lib/map-model.ts`, so the tests toggle and the selection cost no round-trip and the same project always draws the same picture. Svelte Flow supplies pan, zoom and fit; never a layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
87afc50e76 |
feat(ui): the search palette, entry points and a trail that survives the URL (CG-45)
Search: `/` or ⌘K focuses the box; results arrive grouped by kind with their
glyph, signature and file:line, ↑/↓/Enter walk them, Esc dismisses. A group
appears where its best result did, so flattening the groups reproduces the
ranking the keyboard walks — the panel's flat item list IS that concatenation.
A flow question ("how does X reach Y", "X -> Y") is recognised and searches
both endpoints with a note, rather than offering a row that would land on the
phase-2 Flow view.
Entry points answer "where do I start" on the empty screen and in the resting
palette, all derived from the graph: routes, files that run something at module
level (the engine records a top-level statement as an edge out of the file node,
which is what makes src/bin/codegraph.ts the root of the CLI flow — ranked by
calls x the files they reach, so a registration table calling into itself does
not outrank the CLI), and the most depended-on symbols. Tests are excluded from
both derived lists.
Trail: hops record the direction they were walked (→ into a call, ← up to a
caller), clicking one truncates back to it, Clear keeps the place instead of
throwing it away, and the whole walk travels in the URL. A shared or reloaded
trail arrives as ids, so hops learn their names back through a new batch
endpoint and a session name cache — without it, walking back across a
truncation redrew earlier hops as raw hashes. "Read as flow" stays hidden until
there is a Flow view to send it to.
New endpoints: /api/entrypoints and /api/nodes. New engine reads:
getTopCallingFiles, getFileDependentCounts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5cecaabfc2 |
feat(ui): the Symbol view — callers, gutter-ported source, line-anchored callee rail (CG-44)
The core screen of `codegraph ui`: who calls a symbol on the left, its verbatim body in the middle with a port on every line that has an outgoing edge, and what it calls on the right — each callee row placed beside the line that makes the call, with a hairline connector between them. The callee rail is the part that is not a list. A row wants to sit at the centre of its first call-site line and is pushed down only when that would collide with the row above, so the rail keeps source order; the connector still runs to the real line, so the displacement is visible rather than silent. Positions come from measuring the laid-out DOM, so they are recomputed on resize, on font load and whenever a fold opens. Honesty is carried in the drawing, not in a footnote: a filled port means the resolver matched something on that line and a hollow one means it only guessed; uncertain connectors are dashed and their targets fold away behind their count; synthesized edges are dashed differently and tagged with the mechanism that made them; references that leave the index are text with a soft underline rather than links to nowhere, and they are counted. Long bodies keep their head plus a window round every call site — windowed on graph edges only, since a function calling `console.log` two hundred times would otherwise window round every line and buy nothing. Containers over 80 lines show a members outline with per-member fan-in/fan-out instead of 700 lines of braces. Two small additions to the read-only API this needed: * `/api/node` gives every outline member its own fanIn/fanOut (two batched queries for the whole outline). A class's own fan-out is nearly always zero because its methods do the calling, so without these the outline cannot say which member carries weight. * `/api/stats` gains `blastScale` — the denominator the blast bar is drawn against, so one symbol's radius reads as wide or narrow *for this repo*. It is measured across the index's 24 most-depended-on symbols (found with a new `getTopDependedOn`, distinct dependents rather than edges), memoised against the index stamp, and reported as sampled; a symbol wider than the sample becomes the scale instead of overflowing the track. Verified against a real index in a real browser: parity with the prototype on `CodeGraph.sync` (259 lines, 27 callee rows, no overlaps), `GraphTraverser` (20-member outline), a 773-line function (26 windows, 78 connectors), light and dark, hover linking in both directions, keyboard-only navigation, and reflow on resize and on fold toggles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
951ba3678a |
feat(ui): read-only JSON API over the index for the viewer (CG-42)
Six endpoints under `/api/`, one per screen, each answering in a single
round-trip in the spirit of `codegraph_explore` — the viewer should never
have to ask a follow-up question to finish drawing a pane:
/api/stats index state, graph counts, frameworks
/api/search?q= ranked, kind-grouped symbol search
/api/node/<id> rails, members, tests, blast radius
/api/source?file=&from=&to= verbatim source + a drift verdict
/api/file/<path> outline and import rails
/api/routes URL -> handler, when there is one
It is a reader of the existing schema: no extraction or resolution changes.
It mounts on the `api` seam `startUiServer` already exposed, so it sits
behind the CG-41 loopback boundary — Host allowlist, no CORS headers,
GET/HEAD only — and every read out of the repository goes through
`resolveProjectFile`, ahead of the index lookup so a traversal is refused
as a traversal rather than reported as "not indexed".
Three properties the endpoints are built around:
- No N+1. The engine's busiest symbol has 545 incoming edges; resolving
those one `getNode` at a time is 545 queries. Every edge list is
resolved with one batched lookup, which needed four additive read-only
query methods (`getNodesByIds`/`getFanIn`/`getFanOut` on `CodeGraph`,
plus batched outgoing/incoming edge fetches and unresolved-reference
reads). `/api/node` on `LRUCache.get` answers in ~10 ms.
- Capped lists, honest totals. 545 callers cannot all be rows, so caller
groups cap at 300 — but `total` is always the real number, and the
ordering puts the useful end first (same file, then production code,
then tests). Every count in the payload is the length of a list the
same payload returns, so a badge and its rail cannot disagree.
- Nothing overclaims. Source that drifted on disk since the last index
sync is omitted rather than sliced at line ranges that may now point at
a different symbol; calls that leave the index are counted instead of
silently shortening the callee rail; imports that never resolved are
named; and a test-coverage claim reports whether its search actually
finished. `/api/routes` says a project simply is not routed, and
refuses a `limit` below three because the engine's manifest would
answer that question wrongly.
Tests: 45 against a real indexed fixture over a real loopback server,
covering every endpoint's shape, the drift verdict in all three places it
surfaces, search ranking and the filter grammar, the refusals, and the
capping/latency behaviour at 500 callers. The issue's own acceptance case
— `lru-cache.ts` `get` under 100 ms — runs against this repo's index when
one is present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
1d9de88ef1 |
feat(config): add codegraph.json "deprioritize" for ranking-only path down-weighting (#982) (#1463)
* feat(config): add codegraph.json "deprioritize" for ranking-only path down-weighting matchesNonProductionDir hardcodes example/sample/fixture/benchmark/demo, so a peripheral tree only the project knows about — optional-skills/, scripts/ — gets no de-prioritization. When helpers there carry generic symbol names, an exact name match hands them a large bonus and they crowd out the product code that answers the query (#982). deprioritize is the RANKING counterpart to exclude: those paths stay indexed and findable, they just stop outranking first-party code. It is deliberately distinct from the corpus-frequency discount, which keys on a name being common and is near-inert on #982's own repro where only two symbols are named usage. The -15 path penalty alone is not enough, and measuring showed why: on that repro a usage() helper sits at 74.8 against 51.2 for the top product symbol, so -15 lands at 59.8 and still leads. The path penalty is additive and the name bonus it must counter is additive and larger. A de-prioritized path is saying its symbol NAMES are not the answer, so the exact-name bonus is damped to 0.25x there as well — damped, not zeroed, so the tree still ranks when it genuinely is what you asked for. Refs #982 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKXAJMrVrdHS5Uco6ABtky * fix(config): read deprioritize lazily and apply it in explore too Review of the first cut found two real defects. The matcher was built once in wireLayers(), which runs only from the constructor and from reopenIfReplaced(). The MCP server keeps one CodeGraph per project root alive for its whole lifetime, so editing codegraph.json appeared to do nothing until the process restarted -- exclude and include do not behave that way. The predicate now reads loadDeprioritizePatterns() per call (mtime-cached, one stat) and memoizes the compiled matcher on the pattern array's identity. A regression test writes the config after opening the project and fails on the old code. Explore passed no matcher to scorePathRelevance at either of its two call sites, so the setting only half-applied -- and #982's reproduction rows B, C and D are all codegraph explore, which made this the surface the issue actually reports on. Both sites now pass it. Explore's hard early-continue filters and its non-production budget cap are deliberately NOT joined: those REMOVE content, and deprioritize is a ranking lever by definition. README narrowed accordingly -- it previously claimed this extends the built-in list, which overstated it. Also from review: scorePathRelevance takes a boolean rather than a predicate (the caller already evaluated it, and it was being invoked twice per result), the predicate body is exception-guarded so a bad path can never take a search down, the misplaced const moved out from between imports, two vacuous test assertions tightened, and tests added for the single-penalty invariant, the deliberate isTestQuery asymmetry, and a query that genuinely targets the de-prioritized tree. Refs #982 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKXAJMrVrdHS5Uco6ABtky * fix(search): derive the deprioritize name-bonus damping instead of picking it (#982) The 0.25 scale was a guess. On a 62k-node django index it measurably breaks the "discount, don't erase" rule the lever is built on: exact-name queries for symbols that live only in the de-prioritized tree (child, parent, method) fall behind mere prefix matches (children, all_parents, method_decorator). The prefix arm of nameMatchBonus tops out below 40, and a de-prioritized node also takes the -15 path penalty, so 80 * SCALE - 15 > 40 is the bound that keeps a damped exact match ahead of a prefix match at any corpus shape. 0.75 clears it; crowd-out removal is nearly identical to 0.5 (39 vs 40 of 88 peripheral top-10 slots cleared on django), so the deeper discount bought almost nothing and cost the invariant. Two tests pin the bound, including one that fails at the old 0.25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
81e1f4a92f |
fix: harden daemon and large-index recovery paths (#1562)
* fix: harden indexing recovery and daemon liveness * test: cover daemon and recovery review gaps * test: pin that a failure marker never blocks a later successful parse (#1557 retry-discard guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
238dbc5cec |
fix(explore): accurately resolve query file paths and find camelCase symbols
Previously, `codegraph_explore` queries explicitly naming files by path (e.g., `src/routes/m/projects/[id]/runs/[runId]/+page.svelte`) were shredded. Bracketed path segments exploded into "named symbol" seeds, and FTS on fragments like `page` or `runs` admitted every sibling file, starving the user's intended target. This change introduces: - **Query path pinning:** File paths named in a query are now resolved against the index, "pinned," and stripped from the query. Pinned files are guaranteed inclusion, top ranking, and fair allocation. Unresolvable path-like spans are reported. - **Segment vocabulary supplement:** Natural language query terms (e.g., "auto-scroll to bottom") can now reach camelCase identifiers (e.g., `pinFeedIfNearBottom`, `feedAtBottom`) by matching against their constituent segments. - **Variable seeding:** `variable` and `constant` node kinds are now included in identifier seeding, improving recall for `$state`-style variables common in frameworks like Svelte. |
||
|
|
2c708caf7c | Merge branch 'main' into feature/CG-35 | ||
|
|
9efae0f8f2 |
fix(explore): damp ambient declaration files on flow queries (CG-28)
A file that declares nothing but types and that nothing in the index depends on — a hand-written ambient `.d.ts` of global shims, vendored typings, module augmentation — cannot answer a flow question: no bodies, no call edges, no behaviour, nothing typed by it. But the identifiers it declares are exactly the generic ones a prose question uses (`Body`, `Message`, `ImageMetadata`, `ReadableStream`), so on term overlap it out-scored the implementation. Measured on the new fixture: rank #1 and 51% of delivered source, with the flow's own entry file pushed out of the response entirely. Measured first, per the issue: the Wrangler `worker-configuration.d.ts` that opened this is already handled by CG-25's banner detection, worth 15-46 points of envelope share across four flow queries. CG-25 credited; only the un-bannered case needed anything. `rankPenalty` now multiplies score and graph mass by 0.5 for such files, taken as the STRONGER of it and the generated penalty rather than multiplied — one property two signals see must not be charged twice. Detection is structural, not by extension, and four conditions deep. Two of them were forced by measurement: requiring every symbol to be type-level takes the corpus flag rate from 1-18% (which swept in Kotlin sealed classes, Rust mod.rs re-exports and django's locale tables) down to 0-4%; requiring that nothing depends on the file separates an ambient shim from a working types module, and without it the rule demoted displacement-ts's pipeline `types.ts` and broke the CG-31 gate. A query that NAMES a declared type is exempt, so a question about a type still reaches its declaration at full weight. Precise tokens only, so "…the file body…" cannot exempt a `Body` interface it never meant to name; this needs its own set because `namedSeedIds` is callable-only and a type never becomes one. Regression evidence in docs/benchmarks/explore-declaration-only-cg28.md: 6-repo envelope sweep byte-identical against a clean baseline build, zero ambient files reach the candidate set on VS Code across five queries, corpus flag rate 0-0.74%, both allocation fixtures PASS, full suite 2,978 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
03893b0ab9 |
CG-33: converge incremental sync with a full rebuild
A live, auto-synced index did not converge to a clean rebuild of the same tree — 4.3% of distinct edges wrong in both directions on this repo's own index, overwhelmingly `calls`, which is what flow queries traverse and what explore's file ranking weights. Silent: nothing warned, and the symptom read as "codegraph isn't very good" rather than "this index needs rebuilding." Two causes, and the fix needed both. Resolution binds a reference to one of the same-named definitions PROJECT-WIDE, so a definition appearing or vanishing changes the correct answer for references in files the sync never touches — and those references resolved successfully once, which deletes their unresolved_refs row, leaving nothing to revisit them with (#1240's retry only revisits refs parked as failed). Separately, when nothing disambiguated the candidates the winner came down to rowid, i.e. the order files happened to be WRITTEN, which differs between a scan-order full index and a sync that appends each file as it changes. That second one is why re-resolution alone could not converge: re-resolving against the identical graph still picked a different candidate. So getNodesByName now orders by (file_path, start_line) — a property of the code, not of the write order — and sync computes a definitionDelta and re-opens the resolution edges whose answer it may have invalidated, re-inserting each as the reference that created it for the orphan sweep to bind against the post-sync graph. The delta compares `file\0name` pairs per file rather than one name set over the batch: a commit that adds `collect` to a new file while an unrelated changed file already defines `collect` cancels out of a batch-wide set, and that miss was the largest residual class in the first measurement. Conservative where the failure modes are asymmetric — a wrong deletion is a permanent edge loss, a missed rebind is only residual drift. Edges without a refName stamp are never touched (nothing to restore them from), sources the sync already re-extracted are skipped, and a per-name ceiling declines the generic names. Edges are deleted before the sweep re-inserts, since INSERT OR IGNORE against idx_edges_identity would otherwise keep both rows when a reference rebinds elsewhere. Replaying real commits of this repo through sync, then diffing against a rebuild: 16 commits 48 -> 0; 80 commits 1,634 -> 361, with the actively misleading direction (stale edges the index keeps asserting) 671 -> 2. Index and sync wall-clock are unchanged; the ORDER BY costs 18% per uncached name lookup, which never reaches wall-clock because the resolver memoizes it. The 357-edge residual at 80 commits is one pre-existing class: refs to generic names (`push`, `join`) parked above #1240's per-name retry ceiling, which a rebuild resolves into cross-language garbage — a TS test file "calling" an R method. Converging there would mean manufacturing wrong edges, so it is left alone. And no drift metric in `codegraph status`: it cannot be computed without the rebuild it would be recommending, and a proxy would fire on that residual and train users to ignore it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
16e17495f4 |
feat(extraction): content-based generated-file detection (CG-5, #1500)
`isGeneratedFile` was path-only, but Go's own convention is a CONTENT marker (`// Code generated by <tool>. DO NOT EDIT.`), not a filename one. A Go monorepo with generated CRUD in ordinarily-named files sitting beside hand-written use-cases was therefore invisible to every generated-file down-rank in the codebase — that is #1500. Measured on kubernetes/client-go (2,453 Go files): the canonical banner appears in 2,001 of them, the path check flags 0, the new content check flags exactly those 2,001 — no false positives, no misses. Design: decide at INDEX time (content is already in memory for parsing), persist on `files.generated`, read from the DB. Explore never reads file headers per request. - `hasGeneratedHeader(content)` recognizes the standard banners — Go's, protoc's, `@generated`, `<auto-generated>`, Thrift, OpenAPI Generator, FlatBuffers, bindgen, ANTLR. Precision-first and fenced three ways: an 8KB/60-line header window, a comment-line requirement (leader or open block comment), and markers tight enough that prose can't trip them. A generator's own source, holding the banner as a string constant in its body, is not flagged; neither is this module itself (pinned by test). - `isGeneratedFile(path)` is unchanged — cheap, sync, still the fallback. - Schema v9 adds `files.generated` + a PARTIAL index. DDL only, no backfill: the flag derives from content the migration cannot see, so rows stay 0 until a re-index and every reader unions the flag with the path check — an un-migrated index keeps pre-#1500 behavior rather than regressing. Re-index required; noted in the CHANGELOG. - `generatedPredicateFor(paths)` gives ranking a bounded probe + O(1) lookups. Bounded, not cached: no invalidation, so a ranking call can never serve a verdict the last sync already replaced. Wired into explore ranking, findSymbolMatches, findAllSymbols, search (MCP + CLI), the context formatter, and the dominant-file/route-file hygiene filters. Cost (acceptance bar was no measurable index-time regression): a single unanchored `/generat/i` test over the header rejects ~every hand-written file before any line splitting. 4.6 µs/file on client-go (worst case — 82% generated). End-to-end `codegraph init` on client-go, n=3 alternating arms: 5.73s median with detection vs 5.76s path-only baseline; the arms cross over between runs, so the difference is inside run-to-run noise. Scope note: generated status remains a stable TIEBREAK at equal score, exactly where it was. Making it a strong negative signal is CG-10, which this unblocks by making the signal correct and available. Two pre-existing tests hard-coded schema version 8; both now track CURRENT_SCHEMA_VERSION (or the migration table) so future migrations don't require editing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02c0e2c935 |
fix(db): stop watchdog-killed sessions from leaking the SQLite WAL without bound (#1431) (#1490)
A SIGKILL'd process (the #850 liveness watchdog, OOM, a crash) leaves its WAL on disk; the next session appends to the same file; and nothing ever truncated it — PASSIVE checkpoints fold frames but keep the file at its high-water mark, and the one shrinking path (a clean last-connection close) is exactly what a killed-daemon world never takes. Observed at 25.6 GB on a 5.46 GB DB, growing until the disk filled. - journal_size_limit on every connection: resetting checkpoints now clip the WAL back to the cap instead of leaving it at its high-water mark. - healOversizedWal() fired from every DatabaseConnection.open: off-thread PASSIVE fold + TRUNCATE when the leftover WAL exceeds the cap (64 MB, CODEGRAPH_WAL_HEAL_MB to override). Single-flight per connection with bounded retries — concurrent passes defeat each other (each checkpoint sees the other as a busy reader). - Daemon/direct MCP watchdogs now pass progressPaths (DB + WAL), extending the #1231 slow-disk deferral to the long-lived server so a healthy daemon mid slow statement isn't SIGKILL'd — fewer kills, fewer leaked WALs. - codegraph status shows WAL size (human + JSON) and warns when it dwarfs the DB; daemon.log lines and the watchdog kill notice now carry ISO timestamps so kills can be placed in time. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c74e8b05e0 |
perf(sync): adaptive quick-fire debounce + scoped watcher sync — save-to-graph well under a second at any scale (#1397)
Two changes to the watcher path (the always-on daemon every agent session uses), which previously paid a flat 2s debounce plus a full-tree scan-diff on every save even though the OS events name the exact files: 1. Adaptive debounce: a pending set of ≤2 files fires after a 300ms quiet window; ≥3 keeps the full configured window so agent multi-file bursts coalesce exactly as before. Re-arming preserves trailing-edge semantics; a user-set CODEGRAPH_WATCH_DEBOUNCE_MS remains the authoritative upper bound (quick window never exceeds it, floor 100ms). 2. Scoped sync: watcher-triggered syncs pass their pending paths, and the reconciler stats exactly those — per-path logic identical to the full walk (stat pre-filter, hash confirm, the #1240 removal/resurrection flow) — skipping the O(repo) scan and tracked-load. Strict fallbacks keep the full scan-diff as ground truth: directory removals (#1285 — the events can't name the children), empty pending sets (retry paths), and >500-file storms (branch checkouts, which also self-heal anything event coalescing dropped). filesChecked counts examined PATHS so a deletion-only scoped sync can't mimic the #449 lock-unavailable signature. Measured (warm in-process, the daemon path): dubbo one-file sync work 512→335ms, Swift compiler (27k files) 884→385ms — save-to-fresh-graph ≈0.6-0.7s end-to-end including the quick debounce, from ~2.5-6s perceived before. Gates: scoped-vs-full dumps byte-identical on dubbo AND the Swift compiler; watcher suite 30/30 (3 new: scoped pass-through, dir-removal fallback, quick-fire timing); sync suite 34/34 (4 new scoped-parity cases incl. delete-resurrection and the lock signature); full suite 2,696 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ce0ae30e09 |
perf(store): resolution ref-index window — kernel-scale resolution 423→276s, 8c envelope ≈11min (§4d round 2) (#1369)
Store-architecture arc round 2. The batched resolution loop reads unresolved_refs ONLY through the status index + the PK keyset pager; the other five ref indexes (from_node, name, file_path, from_name, failed_tail) serve sync-time paths — yet every per-batch DELETE of resolved refs maintained all of them, the biggest single main-thread stage on the dubbo profile (deletes 1.2s of a 5.4s resolution phase) and 50-81s at kernel scale. beginBulkRefLoad/endBulkRefLoad on DatabaseConnection, threaded as refIndexLoad hooks next to the existing bulkEdgeLoad pair with the same minRefsForPool gate (small syncs never pay): drop the five for the loop, rebuild each in one scan at the end — where the table holds only the surviving FAILED refs (resolved rows are deleted by then), so the recreate is near-free. Crash inside the window heals on the next open (schema.sql re-applies CREATE INDEX IF NOT EXISTS). Measured: - dubbo: deletes 1.2 → 0.2s, marks 0.6 → 0.3s, recreate 219ms; wall ~8.5s flat — the freed main-lane time shifts into settle (the worker lane now binds the double-buffer at medium scale). - Linux kernel 8c: resolution 423.4 → 275.9s (deletes 50-81 → 3.2s, backpressure 16.8 → 7.4s — fewer index writes mean less WAL and cheaper folds), ref recreate 10.3s. Envelope ≈ 11.0min, from the 14.8min pre-arc best; <10min-on-8c now needs ~1 more minute. Gates: dubbo/gson dumps byte-identical; linux counts exact 2,049,153/6,413,518 and dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (153 files / 2588 tests). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f6d8e8fdab |
perf(store): parse-lane index deferral — dubbo fresh init −19%, kernel-scale envelope best-ever 14.2min (§4d round 1) (#1368)
Store-architecture arc round 1 (the cbm speed bar: dubbo warm wall 10.7-11.2s vs their ~7.5). §4d measured dubbo's parse-loop as 94% store-writer busy with B-tree maintenance as the floor (statement batching and sorted inserts already killed at ~zero). This applies the resolution phase's proven edge-index window to the whole parse lane: beginBulkParseLoad/endBulkParseLoad on DatabaseConnection — FRESH-INIT ONLY (incremental runs delete per-file rows through the file_path indexes) — drop all 15 nodes/unresolved_refs/files secondary indexes plus the 4 non-unique edge indexes for the parse phase's mass insert (the UNIQUE edge identity index stays: OR-IGNORE dedup conflicts on it, and its source prefix keeps mid-window reads indexed), then rebuild each in one table scan before resolution, with a yield between builds (the endBulkEdgeLoad watchdog rationale). A crash inside the window heals on the next open — schema.sql re-applies CREATE INDEX IF NOT EXISTS. Measured: - dubbo (cbm bar repo): parse-loop 4,306 → 1,787ms (−58%), rebuild 665ms, warm fresh-init wall 10.5-11.3 → 8.46-9.39s (−19%); the bar gap vs cbm shrinks from ~3s to ~1.1s. - Linux kernel 8c: envelope ≈ 14.2min, best ever (prior 14.8). Parse itself flat (linux parse is extraction-bound, not writer-bound) and the rebuild costs 21.6s — but every downstream phase dropped (resolution 517-589 → 423.4s, edge-recreate 36.5s, synthesis 157.1s, maintenance 16.3s): bulk-rebuilt B-trees are densely packed where incrementally-grown ones are fragmented, so every index-mediated read for the rest of the run pays fewer pages. Gates: dubbo/gson/express/excalidraw full dumps byte-identical (dubbo's canonical 441,270 lines reproduced); linux counts exact 2,049,153/6,413,518 and dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (153 files / 2588 tests). Incremental sync paths untouched by construction (freshDb gate). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7cc23668b5 |
perf(resolution): batch-loop de-quadratic — keyset reads, changes-based guard, DB-scaled valve caps + resolve profiler (#1339)
The §7a.2 per-ref profile overturned the assumption the whole arc was built on: resolveOne owns only ~93s of the kernel-scale ~433s batch loop. Loop-stage attribution (CODEGRAPH_RESOLVE_PROFILE, shipped here) named the rest: backpressure folds 111.2s, count guard 93.9s, batch reads 54.6s, deletes/inserts/marks ~84s, settle 85.7s. - Non-progress guard O(remaining)→O(1): the per-batch COUNT(*) walked every remaining pending row (O(N²/batch) per run, 93.9s). The cleanup queries now return summed SQLite , and zero-removals-from-claimed-work is the guard signal — the DIRECT evidence the count diff inferred (a mismatched-name resolver makes keyed cleanup no-op ⇒ changes=0). A real COUNT runs only on that suspicious path and arbitrates exactly as before. - Batch reads OFFSET→keyset (54.6s→O(batch)): OFFSET re-walked the accumulated failed-row prefix every read; seeking past the last-seen rowid is prefix-independent and enumeration-order identical. - WAL valve caps scale with DB size (env still wins): every fold re-writes hot pages (#1231 in bounded form — 111.2s at the flat 256MB cap); soft=clamp(dbSize/4, 256MB, 2GB) trades ~4× fewer folds for a transient WAL ≈ project size. - CODEGRAPH_RESOLVE_PROFILE: per-outcome resolveOne histogram + loop-stage attribution, main + workers, off by default. Gates: dubbo dump byte-identical; suite 2,491 passed / 4 skipped (kernel required). Kernel-scale payoff run lands in the plan doc next. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6e52295ceb |
fix(resolution): WAL containment for the pooled superphase — writer backpressure at pool-idle boundaries (#1332)
At kernel scale the pooled resolution/synthesis superphase grew a 22GB WAL on a 4.6GB DB (cg1212, §7a.1): autocheckpointing is deferred for the run, and the valve's timer-driven passive checkpoints stay perpetually partial against the pool's continuous reads — no mechanism ever completed a backfill, so the WAL accreted the whole phase's write volume, blowing disk and feeding page-cache pressure into the 8-core/7GB container OOM. The valve's writer-side backpressure() hard-cap backstop existed but was wired only into the PARSE orchestrator. Thread it into the resolution batch loop at the double-buffer's one pool-idle boundary (batch settled, next not yet fanned out), after the edge-index recreate, and through the synthesis insert loops. Parked there, the backfill completes; readers re-enter at SQLite's backfilled mark and the next persist commit wraps the WAL. Dubbo validation, same build: valve@16MB peak WAL 251MB (floor = the single-transaction edge-index recreate) vs defaults 914MB; dumps byte-identical (441,270 rows); wall unchanged (11s). Suite 2,479 green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
567b4ad4be |
perf(resolution): drop non-unique edge indexes during the bulk resolution window, byte-identical graphs (#1322)
The resolution persist's measured cost is B-tree maintenance on the edges table's five indexes (offline replay of a 224k-edge resolution set: 2.8s with all indexes, 1.1s with only the unique identity index, +0.3s to recreate the rest). On big runs (same >=150k-ref gate as the resolver pool) the four non-unique edge indexes are now dropped for the batch loop and recreated in one pass each before synthesis. Why this is safe: - idx_edges_identity stays: INSERT OR IGNORE's dedup conflicts on it (#1034), and its leftmost column is `source`, so the only mid-window edge reads — resolution's supertype walks (implements/extends by source) — keep an index via its prefix (verified with EXPLAIN QUERY PLAN). - The window closes BEFORE synthesis, whose passes read kind-keyed, and on every error path (finally). - A crash inside the window heals on the next DatabaseConnection open — schema.sql re-applies CREATE INDEX IF NOT EXISTS, same recovery as the FTS bulk-load pattern this mirrors. - Concurrent readers (a daemon serving the project mid-index) stay correct; target/kind-keyed reads degrade to scans only for the window's duration. dubbo (4,402 files): persists 4.0s -> 3.0s, fresh init 11.9s -> ~11.1s, graph byte-identical. excalidraw (below the gate): untouched, byte-identical. Recreation cost ~250ms, logged under CODEGRAPH_SYNTH_TIMINGS as edge-index-recreate. Suite green (2444). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2f3c31a97 |
perf(resolution): defer checkpoints and double-buffer persist during resolution, byte-identical graphs (#1320)
Fresh init on a 4,402-file Java repo (dubbo): 18.2s -> 13.5s (-26%), with the resolution phase going 12.6s -> 7.9s (-38%). Graphs verified byte-identical on both the pool path (dubbo) and the sequential path (excalidraw). Two changes: 1. The fastInit+pool path restored WAL for the resolver workers but left wal_autocheckpoint at its default, so the persist loop inline-checkpointed hot pages all phase long (#1231's pathology inside resolution — measured at 58% of resolution wall). Checkpointing is now deferred behind the bounded valve and folded once at maintenance, mirroring the deferWal path. 2. The resolution loop is double-buffered: batch k+1 is prefetched (OFFSET past batch k's still-pending rows, under an explicit ORDER BY rowid) and fanned out across the pool while batch k's ref cleanup runs on the main thread. Batch settle-waits dropped 2572ms -> 117ms. Correctness invariant found by the byte-identical gate and now documented in the loop: batch k+1's resolution READS batch k's edges (resolveMethodOnType walks supertype chains over extends/implements edges that resolution itself inserts), so edges must persist BEFORE the next batch fans out; only the ref cleanup overlaps. Also extends the CODEGRAPH_SYNTH_TIMINGS instrumentation with phase labels (grammar-init, parse-loop, fts-rebuild, resolver-reinit, resolution, callback-synthesis) and pool timings (worker open/resolve, per-batch mode, persist), so the next profile is one env var away. Suite green (2444). Sync path timings unchanged. Pool floor re-validated: forced-on at 40k refs is still net-slower, so the 150k threshold stands. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1de7e8f8b5 |
fix(retrieval): multi-hump field-name queries reach their definers (#1319)
Three compounding defects (#1196) made a query bag of object-literal keys (`profileInfo isTrialEligible quotaInfo billingMethod`) return unrelated results while the defining files never surfaced: 1. Step 5b title-cased interior humps (profileInfo -> Profileinfo) and then compared case-SENSITIVELY, dropping every row SQLite's case-insensitive LIKE had just recovered. The hump lookup is now case-insensitive with an explicit uppercase-at-match requirement. 2. Step 5b/5c's kind whitelist held only type-like kinds — dead code on method-centric codebases. Callable kinds (function/method/component) are fetched as a SEPARATE LIKE batch so hot single-word terms can't crowd classes out of the length-ordered 200-row batch. 3. explore's named-symbol seeding was exact-name only; a field token seeded nothing. A camelCase token with ZERO exact defs now seeds its camel-infix definers (callables, hump-boundary or prefix, shortest first, capped at 3) — bare lowercase words keep the #1252 stopword guard untouched. The reporter's acceptance query is a pinned e2e test (definer files present, exact-name seeding unaffected). excalidraw probe: the canonical flow query (mutateElement renderStaticScene) is byte- identical; NL queries shift toward more-central callables (useUIAppState/getDefaultAppState over observer periphery). Fixes #1196 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18f0745f81 |
perf(sync): defer WAL autocheckpoint for the whole incremental run (#1312)
The #1242 fix (WAL deferral + checkpoint valve, the 26x win on HDD-class storage) was wired only into indexAll. CodeGraph.sync never touched wal_autocheckpoint, so every incremental run kept the default 1000-page cadence and re-triggered the #1231 per-page checkpoint thrash — a 7-file sync took 2m 2s at 0-2% CPU on the reporter's hardware, because the cost scales with the EXISTING database's hot pages, not the change size. sync now mirrors indexAll exactly: defer autocheckpoint + start the valve for the run, fold the store phase's WAL before the post-store reads, restore the interval in the finally. Same kill switch (CODEGRAPH_NO_WAL_DEFER=1). Idle valve cost is one timer, so watcher-frequency syncs stay cheap. Fixes #1248 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5736e24bb6 |
perf(index): faster fresh indexing + parallel reference resolution, byte-identical graphs (#1305)
* perf(index): ~34% faster fresh indexing, byte-identical graphs Profiling a fresh init on a medium TS repo (excalidraw, 657 files) showed the main thread as the critical path: per-row SQLite statement calls, repeated import-resolution walks, and per-row FTS trigger firings, with the parse workers ~75% idle behind it. This lands the semantics-preserving tranche of fixes: - Multi-row batched INSERTs (nodes/edges/unresolved refs/name segments) behind cached per-batch-size prepared statements; row order preserved, so rowid-based resolution determinism (#1015) is unchanged. - storeFileBundle: one transaction per file instead of four; nested transaction() calls now flatten (BEGIN-in-BEGIN previously threw, so no caller depended on nested rollback). - Dedicated store-writer thread for the fresh-DB bulk path (bundles applied in file order on a single writer connection; main thread does no DB work during the parse loop). Kill switch: CODEGRAPH_NO_STORE_WORKER=1. - Bulk FTS mode: drop the nodes_fts sync triggers during the bulk load, rebuild once at the end; crash inside the window self-heals on the next open. - Per-context memos for resolveImportPath/findExportedSymbol + a per-file exported-symbol index, invalidated exactly where clearCaches() already resets the resolver's own caches. - Fast-init on completely fresh DBs (journal in memory, no fsync until the index completes; interrupted init re-runs from scratch). Kill switch: CODEGRAPH_NO_FAST_INIT=1. - MaybeYield returns undefined on the not-due path so per-ref yield checks stop paying a promise + microtask hop each. - Parse pool prewarm for bulk indexing; compile-cache enabled at CLI and worker entry points. Excalidraw fresh init: 5.11s -> 3.36s median (n=5, warm cache, M-series). Graph dumps byte-identical across init, re-index, and sync paths; full suite green (2403 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(resolution): parallel reference resolution with canonical admission Fan resolution batches across a pool of read-only worker threads, each hosting a full ReferenceResolver over its own SQLite connection; results are admitted on the main thread in chunk order, so edge insertion order, row cleanup, failure parking, and deferred post-pass queues are exactly the sequence the single-threaded loop produces. Per-ref inputs match the baseline because the sequential path already resolves each batch against the state committed BEFORE that batch. Validated byte-identical on excalidraw (pool forced on) and apache/dubbo (4,048 Java files): dubbo full index 39s -> 19s (2.05x) with identical graph dumps (91,495 nodes / 223,953 edges). The pool only engages when total pending refs clear a threshold (default 150k, CODEGRAPH_PARALLEL_RESOLVE_MIN to tune, CODEGRAPH_NO_PARALLEL_RESOLVE=1 to disable): measured on a ~58k-ref repo the workers' boot CPU contends with resolution on the same cores and makes indexing slower, so small repos keep the sequential path. When fast-init left the DB in memory-journal mode, WAL is restored before resolution only when the pool will run (readers + rollback-journal writers don't mix). Also: sqlite adapter readOnly open support. TreeCursor spine rewrite of the body walker was built, measured neutral on real repos and equal in a 20k-child microbench (web-tree-sitter's namedChild(i) is not quadratic in this binding), and rejected — per-node JS<->WASM marshaling is the floor, which a traversal swap cannot remove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad5300a601 |
fix(ui): show synthesis as a 'Linking dynamic dispatch' phase; mute node:sqlite warning spam (#1299)
Two first-run UX bugs surfaced by indexing a real 1,342-file C repo: 1. After 'Resolving refs' hit 100%, the ~40 dynamic-dispatch synthesis passes ran with no progress surface, so the bar sat frozen at 100% long enough to read as a hang (the C fn-pointer pass alone can hold for a while on C-heavy repos). Synthesis now reports per-pass progress through a new 'linking' IndexProgress phase, rendered as 'Linking dynamic dispatch'. The step total is pinned by a test to the synthesizer's actual __mark() count so adding a pass without bumping it fails loudly. 2. node:sqlite's ExperimentalWarning is emitted once per THREAD, so the main process plus every parse worker printed it mid-index, interleaved with the progress UI. All launch paths now pass --disable-warning=ExperimentalWarning: both bundle launchers, the Windows npm-shim invocation, and the CLI self-relaunch (NODE_RUNTIME_FLAGS, deliberately excluded from the re-exec gate so an older installed launcher never triggers a pointless re-exec, and version-gated off nodes older than the flag). Verified end-to-end on the same repo: zero warnings, live linking bar, byte-identical graph (50,520 nodes / 148,232 edges). Full suite green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d0cd3a7d1 |
fix(sync): resolve cross-file refs when an edit adds or removes the satisfying symbol (#1240) (#1249)
* chore: ignore .kommandr/ directory * fix(sync): resolve cross-file refs when an edit adds or removes the satisfying symbol (#1240) Incremental sync scoped reference resolution to the changed files' own refs, and a completed pass deleted every ref it failed to resolve — so a symbol change in one file could never repair references in UNCHANGED files, in either direction, until a full re-index: - New-export case: a.ts imports/calls `greet` before b.ts defines it. The failed refs were deleted at index time; when b.ts later gained `greet`, nothing revisited a.ts — the calls/imports edges stayed missing while status reported a clean index. - Removal case: when a re-index (or file deletion) dropped a symbol, the incoming edges cascade-deleted and the callers — whose resolved refs had been consumed — never got a chance to rebind to an alternative definition or reconnect when the symbol returned. Fix, sharing one lifecycle: - Schema v8: unresolved_refs gains status ('pending'/'failed') and name_tail (last dotted segment, so `h.greet` is findable by `greet`). Both resolver persist paths now park unresolvable refs as failed instead of deleting them. All pending-work readers (batched drain, non-progress guard, #1187 orphan sweep, status pendingRefs) filter to pending, preserving their invariants and keeping status honest. - Sync retry: after scoped resolution, failed refs whose name tail matches a symbol name now present in the changed files are re-resolved through a per-ref-yielding path (watchdog-safe, #1091 class). Names matching >500 failed refs are skipped as external/builtin noise (#999 rationale). - Removal side: createEdges stamps each resolution edge with its originating reference (metadata.refName, + refKind when kind promotion rewrote it). When the #899 restore misses a target or sync deletes a file, the dropped edge is resurrected as exactly that ref — re-resolved in the same sync (rebinding to an alternative definition) or parked failed until the symbol reappears. Edges without the stamp (pre-upgrade, synthesized) still drop silently: reconstructing from the target's plain name would strip receiver context and risk a rebind a full re-index would never make. - Pure-removal syncs clear resolver caches so a long-lived daemon can't resolve resurrected refs against the pre-removal graph. Validated: issue repro now yields a graph byte-identical to a full re-index; move/remove-readd/file-deletion scenarios all rebind or heal; baseline-vs-new A/B on express and gin shows identical node/edge counts and no timing regression (DB grows ~25% from the parked ref rows — pure cache, reset by any full re-index). 8 regression tests added. Fixes #1240 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a11a439002 |
fix(indexing): HDD-class storage — false parse timeouts, dropped files, and WAL checkpoint write-back (#1231) (#1242)
Parse timeouts are now judged by the worker's own clock: the base timer only marks a job late (after a long synchronous store stall, Node runs the timers phase before the poll phase, so the timer fired before an already-delivered result was processed — killing workers over parses that took milliseconds, even on 0-byte files); a result arriving before a 3× hard-kill backstop is accepted, timed-out files are retried, and CODEGRAPH_PARSE_TIMEOUT_MS overrides the budget. Grammar WASM bytes are pre-read once on the main thread and handed to every worker, so spawns/respawns load grammars from memory instead of re-reading a saturated disk. Bulk indexing defers WAL auto-checkpointing for the whole run: the default 1000-page interval re-writes hot B-tree/FTS pages into the main DB file over and over — ~95% of all disk I/O under throttled measurement. A WalCheckpointValve bounds WAL growth with off-thread PASSIVE backfill passes (never blocking the writer or the #850 watchdog heartbeat), pauses the writer for a full backfill if the disk truly can't keep up, and folds the WAL at the parse→resolution boundary so post-parse reads never page a bulk-write-sized WAL. Opt out with CODEGRAPH_NO_WAL_DEFER=1; tune with CODEGRAPH_WAL_VALVE_MB. Measured at 150 IOPS (HDD class): commons-lang 1526s → 59s with 0 dropped files (was 8); guava-scale completes in 7.6 min with a full graph where v1.3.1 needed 25 min for a repo 5× smaller. Unthrottled: no change. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a3f90089e8 |
fix(indexing): bounded-memory yielding pipeline tail + daemon session fixes (#1212) (#1226)
Large-codebase indexing died at the end of "Resolving refs" two ways: watchdog kills of healthy work (24k-file Java on Windows, #1212 — third iteration of the #1091/#1122 class) and hard OOMs (Linux kernel scale, where v1.3.0 could not complete at any watchdog setting). Root causes: ~31 of 37 dynamic-edge synthesis passes ran start-to-finish with no yield points, several materialized whole-graph snapshots (kotlin expect/actual opened with getAllNodes() — 2M nodes in one array; the C fn-pointer pass retained every C file's contents twice plus every function node), and the post-index WAL checkpoint ran minutes of synchronous IO on the main thread, killing even a successful index at the finish line. The pipeline tail now follows the same discipline as the rest: never hold O(graph) in the heap, yield everywhere. - All synthesis passes stream node-kind scans (cursors, not arrays) and yield on time-budgeted checkpoints; language gates skip passes whose filters a project's file languages provably can't satisfy. - kotlin expect/actual filters SQL-side; c-fnptr caches are LRU-bounded, units stream one file at a time, and the all-functions array + write-only id map are gone; spring reads each .java once, not twice. - runMaintenance moved to a worker thread (own SQLite connection); per-file store commits chunk with yields behind a serialized flush chain (preserving #1015 file-order determinism); resolver warm-up streams the DISTINCT name set; resolution batch-tail and merged-edge inserts run in bounded sub-transactions. - Daemon: fixed a socket-handoff race that could leave a fresh MCP session permanently silent (client-hello tail unshifted into a flowing stream with zero listeners — the long-standing #662 test flake was this real bug); first tool call no longer queues behind the query pool's cold start (pool.ready gate). Validation: Linux kernel (70,129 files, 2.05M nodes, 6.4M edges) fully indexes in 27m8s on a 2-core/6GB container at default heap + default watchdog; llvm-project (180k files) completes under 1GB RSS including kill-and-sync recovery; synthesized-edge and full-graph parity are byte-identical vs baseline on elasticsearch/redis/vim; the ex-flaky daemon test passed 25/25 under load. Env-gated diagnostics kept: CODEGRAPH_SYNTH_TIMINGS pass/phase timings, CODEGRAPH_MCP_DEBUG hop tracing. Design record: docs/design/main-thread-stall-followup.md. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4c15f84aa4 |
fix(resolution): sweep orphaned unresolved refs so an interrupted index heals on sync (#1187) (#1191)
An indexing run killed mid-"Resolving refs" (crash, Ctrl-C, the #1122 watchdog kill) left the refs it never reached parked in unresolved_refs. The git-scoped sync fast path only re-resolves changed files' refs, so those files' call edges were missing permanently — a too-small blast radius clustering by package/module (the #1187 field report: 3 of 10 caller files for a Spring @Resource-injected method) — until a full re-index. - sync() now sweeps leftover unresolved refs with the batched resolver after its scoped pass, including on no-change syncs, so a bare `codegraph sync` recovers a wedged index (and heals pre-fix indexes on the first post-upgrade sync) - the scoped pass deletes unresolvable rows too (parity with the batched path), making "rows at rest" a sound orphan signal - drop the batched loop's early break that abandoned all later batches when one batch was all-unresolvable (its rows WERE consumed — that early stop could orphan the rest of the table at init) - surface the state: `codegraph status` warns, `status --json` gains index.pendingRefs, and MCP codegraph_status tells agents the blast radius is incomplete until the next sync Verified end-to-end on a 2,414-file synthetic Spring repo: SIGKILL mid-resolution reproduces the reporter's exact 3-of-10-callers state; a bare sync now heals it to 10/10 with the edge count converging to the clean-init total; a healthy-index sync stays a no-op. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f325134e0 |
feat(extraction): add Nix language support with module-system option wiring (#324, #332 via #648 — carries #1084) (#1190)
Carries @TyceHerrman's #1084 as the functional base. Extraction + file wiring (imports/modules lists, callPackage), module-system option-path synthesizer, lexical-scope resolution gates, ABI-15 wasm rebuilt from upstream source. Validated on agenix, nix-darwin, home-manager, and nixpkgs (44,368 files, 3m49s, 1.30M nodes). Co-authored-by: Tyce Herrman <Tyce.Herrman@pm.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
99152212a9 |
feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)
Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language: full TypeScript-grade extraction via the harmony-contrib tree-sitter grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0 npm tarball), plus the ArkUI constructs that make HarmonyOS apps traceable: - @Component/@ComponentV2 structs with decorators from both grammar positions; members extract as class members with qualified names. - build() component trees: child instantiation edges via arkui_component_expression, no synthesizer needed. - Attribute chains emitted dot-prefixed and resolved ONLY against @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) — bare-name fallthrough produced 36,840 wrong edges (17% of calls) on the OpenHarmony samples monorepo. All four grammar chain shapes handled, including the detached-chain forms. - .onClick(this.handler) method-reference bindings. - ohpm workspace modules: bare imports follow oh-package.json5 file: deps (ambiguous names dropped), honoring each module's main entry — which also lets .ts consumers resolve .ets modules. - ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with wiring-site metadata: assignment-gated state->build() re-render (V1 @State family + V2 @Local/@Provider/@Consumer), @ohos.events.emitter emit->subscriber pairing on static event keys (numeric ids same-file, named constants same-module, fan-out capped), and router.pushUrl literal urls -> the target page's @Entry struct. - $r/$rawfile resource intrinsics treated as built-ins; arkts joins the web language family, value-reference edges, re-export chase, and the other TS-applicable gates. Also ships a language-agnostic index-completeness guard: indexAll stamps index_state (indexing -> complete/partial/failed), reconciles discovered vs accounted files (a loaded run silently dropped 37 files), and codegraph status surfaces truncated/partial indexes in human and --json output. Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693 files, 202,890 nodes stable across re-index, attribute false-positive audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and #988 with credit — both informed this implementation. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35611b92bb |
fix(prompt-hook): close the segment-vocab integrity gaps (#1141, #1142, #1144, #1145, #1146) (#1150)
Five hardening fixes to the #1136 MEDIUM (graph-derived) tier: - #1141: updateNode() now writes the segment vocabulary like insertNode() does — framework post-extract renames (NestJS route prefixing) left the new name permanently unsearchable (the old rows orphaned, the backfill gated on an EMPTY vocab, so even a full re-index re-created the drift). - #1142: new CodeGraph.healSegmentVocabIfEmpty() — the hook opens the graph without sync, so a database migrated from pre-vocab schema kept the MEDIUM tier dormant until some unrelated sync ran. The hook heals on first use (one SELECT when populated; lock-aware, defers to a running sync) and records noop-vocab-empty when it can't. - #1144: a name whose only nodes are file/import kind is skipped instead of falling back to surfacing an import statement as a matched symbol; import specifiers no longer enter the vocab at all (shared isSegmentableKind gate across insertNode/updateNode/rebuild page query) since they can never be surfaced and only inflate rarity statistics. - #1145: plural variant folding is keyed on English plural spelling — bare-s plurals no longer mint a bogus -es sibling (services→servic), unambiguous sibilant-es plurals no longer mint a bogus -s sibling (classes→classe), trailing -ss singulars no longer strip (class→clas); genuinely ambiguous endings (caches/databases) still emit both keys. - #1146: getSegmentCoOccurrence folds variants to their original word inside the SQL (CASE mapping + COUNT(DISTINCT word)) so a plural pair of ONE word can't tie with a genuine two-word match and crowd it past the pre-fold ORDER BY/LIMIT; the JS re-check stays as the honesty layer. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e699ee9686 |
feat(prompt-hook): graph-derived gate tier + confidence-tiered injection + gate telemetry (#1136)
The keyword gate (#1126) can never know a repo's domain nouns. This adds the graph-derived tier the design discussion converged on: symbol names are split into prose segments at index time (name_segment_vocab, riding the insertNode write path), and the hook verifies a prompt's plain words against them — "the state machine des commandes" → OrderStateMachine, in any language whose technical nouns are Latin script. Confidence now decides HOW MUCH to inject, not just whether: - HIGH (keyword, or index-verified code token): full explore injection, unchanged — the validated adoption lever. - MEDIUM (segment matches only): a ~500-byte pointer naming the matching symbols; the AGENT writes the explore query. Never runs explore, so a fuzzy match can't inject 16KB of wrong-feature context. - Silent otherwise, as before. Precision is derived from the repo's own naming statistics plus measured FP fixes: co-occurrence (≥2 words on one name) always qualifies; a single word must be ≥5 chars, cluster across 2–25 names (singletons are prose coincidence: "deploy to production" → matchesNonProductionDir), match a multi-segment name, and not be an English function/filler word (the one place a word list is honest: identifiers are English, so only English prose collides). Every candidate is re-verified against nodes before being surfaced — vocab rows are proposals, deletions leave orphans by design, a full index rebuilds from scratch, and sync heals pre-upgrade databases (batched + yielding; emptiness captured at sync ENTRY so the sync's own writes can't mask the backfill). Schema v7 migration is DDL-only (instant; none of the #1067 row-churn hazards). Gate outcomes roll up as anonymous usage counters (prompt-hook-gate-<outcome>, names only, never content) through the existing telemetry pipeline — recall becomes measurable, and the counters are the agreed kill-criterion data for ever revisiting a local classifier. Co-authored-by: Claude Fable 5 <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> |
||
|
|
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> |
||
|
|
e43ac82cdf |
fix(mcp): reopen the database when it's replaced on disk instead of serving a deleted inode (#925) (#940)
A long-lived `serve --mcp` process opens `.codegraph/codegraph.db` and holds the fd for its whole life. If `.codegraph/` is removed and recreated AT THE SAME PATH while it runs — `git worktree remove <p>` + re-add, or `rm -rf .codegraph` + `codegraph init` — the held fd points at the now-unlinked inode and can never see the new index. The server serves the pre-removal snapshot (renamed/removed symbols still "live", new ones missing); `codegraph sync` can't refresh it and the CLI (a fresh process) diverges. Only a restart fixed it — and because the daemon registry is keyed by path, a same-path recreate routes new clients straight back to the same stale daemon, so the fix has to self-heal inside the running process. - DatabaseConnection records the DB file's (dev, ino) at open and exposes isReplacedOnDisk() — a different inode now at the same path. POSIX-gated: Windows can't unlink an open file and its st_ino is unreliable, so it never fires there. - CodeGraph.reopenIfReplaced() opens the live file first, then swaps the connection + query layers IN PLACE (via the new wireLayers() helper), so every holder of the instance (the daemon's default project, cached projectPath connections) heals without a restart. Closing the dead handle also frees the leaked db/-wal/-shm fds pinning the unlinked inode. - ToolHandler.getCodeGraph calls it (freshen) before serving — one stat() per call, a no-op unless the inode actually changed, never throws into a tool. Tests cover isReplacedOnDisk (unchanged / replaced / absent / Windows-gated) and an end-to-end reopen that heals a held instance after a same-path recreate (asserts the pre-heal staleness too). Validated on macOS with a dist probe of the raw instance and the MCP serving path; full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
beca7116a0 |
feat(mcp): surface degraded watcher state to the agent in tool responses (#892)
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.
Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.
Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
38095aa95b |
feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
Three callback-registration shapes deferred from #756/#808, one arc: 1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>` registration whose member isn't on the enclosing class defers to a second pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs, runs after implements/extends edges persist, same lifecycle as the #750 conformance pass) and resolves up the supertype chain, depth-capped BFS, validated targets only. `bus.on("submit", this.handleSubmit)` in a subclass links to FormBase::handleSubmit; same-named methods on unrelated classes never match. this.-prefixed candidates skip the extraction name gate (an inherited member can't be in definedHere). 2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` / `OtherClass::handle` emit QUALIFIED names resolved by the scoped suffix-matcher — cross-file capable, gated on the scope name being a same-file type or an imported name (dotted JVM imports now contribute their last segment). `this::m` and `super::m` route through the class-scoped resolver (super rides the supertype pass). References through a VARIABLE (`subscriber::onNext`) deliberately produce nothing — receiver type is unknowable; RxJava's baseline bare capture was resolving these to same-named same-file methods (a test method "registering" an anonymous class's onNext) — the rework drops 18 such wrong edges and keeps the 7 genuine Type::method refs RxJava's main tree actually has. 3. SWIFT enclosing-type scoping (implicit self): bare callback names match methods only of the from-symbol's own type (extension/nested scopes reconciled by suffix), and top-level code never matches methods. Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier` resolving to same-named methods on unrelated protocols), all verified; the same-class param collision (`task`) remains and is documented. New ResolutionContext.getNodeById lets matchers derive the from-symbol's class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges changed on any of 7 A/B repos; nodes identical everywhere. Kotlin companion-object members extract unqualified (pre-existing) so `Type::companionFn` stays silent rather than guessing — documented. Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
48d4654e8d |
feat(resolution): conformance-aware chained-method resolution (#750) (#754)
* feat(resolution): conformance-aware chained-method resolution (#750) A chained static-factory/fluent call whose method lives on a SUPERTYPE the receiver conforms to — a protocol-extension method (Swift), an interface default method, or an inherited superclass method — now resolves. resolveMethodOnType falls back to walking the return type's implements/extends edges (via the new context.getSupertypes) when the method isn't a direct member. Because those edges don't exist during the single-pass resolution, a second pass (resolveChainedCallsViaConformance) re-resolves the deferred chained refs after edges are built. Still validated, so a wrong inference yields no edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): conformance-aware chained-method resolution (#750) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
75ae1e8bd9 |
fix(search): down-weight the project name in ranking — completes #720 (#748)
The per-word path fix (#745) brought the backend to parity but not above: the project name still gave the lexically-matching stack a residual dir match + an FTS class-name match, so a backend query that included the project name still ranked the frontend at/above the backend. Derive the project name from go.mod module / package.json name / repo dir, and treat a query word matching it as non-discriminative: drop it from path relevance and from codegraph_explore's PascalCase type-disambiguation bias (reporter's suggestions #1/#2) — unless it's the only query word, so a bare project-name search still scores. Narrow by construction: the down-weighting fires ONLY when a query word matches the derived project name (≥5 chars), so every query that doesn't name the project is byte-identical. On the reporter's repro the backend controllers now top a backend question that includes the project name; queries without it, bare project-name queries, and normal symbol queries are unchanged. Query-time only (no re-index). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a56d9e6941 |
feat(directory): CODEGRAPH_DIR env var to override the index dir name (#636) (#741)
Two environments that share one working tree — most concretely Windows and WSL — can't safely share a single `.codegraph/`: the daemon lockfile records a platform-specific pid + socket (named pipe vs Unix socket), and SQLite locking across the WSL2/Windows filesystem boundary is unreliable, so two daemons over one index risks corruption. Add a `CODEGRAPH_DIR` env var (default `.codegraph`) that overrides the per-project data directory name, so each environment keeps its own index in the same tree (e.g. `CODEGRAPH_DIR=.codegraph-win` on Windows). The name is resolved live and validated (rejects separators / `..` / absolute, falling back to the default with a one-time stderr warning). Indexing and file-watching now skip ANY `.codegraph-*` sibling so neither side trips over the other's data. Routes the previously-hardcoded `.codegraph` literals (db path, lockfile, error log, watcher ignore, file-scan skip, installer) through the resolver. No extraction-version bump — index content is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4e5cf2de56 |
feat(cli): add codegraph upgrade self-update + stale-index re-index hint (#710)
`codegraph upgrade [version]` detects how the CLI was installed — the standalone install.sh/install.ps1 bundle, npm-global, npx, or a source checkout — and updates in place: re-running the canonical install.sh on macOS/Linux, an in-place rename-and-extract swap on Windows (a running node.exe can't be deleted, only renamed, so the detached-helper approach is avoided), and npm/npx/source-specific guidance otherwise. Flags: `--check` (report only), `--force`, and a positional version to pin. Each full index is now stamped with the engine's EXTRACTION_VERSION in project_metadata; `codegraph status` (and `--json`) flags an index built by an older engine and recommends re-indexing, and `upgrade` prints the same reminder. Gated on EXTRACTION_VERSION so it never nags on extraction-neutral releases. Validated end-to-end on macOS (real bundle upgrade), Linux (Docker, real curl|sh) and Windows (Parallels VM, real in-place swap). 32 new unit tests. Closes #679 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b62356f53 |
feat(cli): add version, indexPath, lastIndexed to status --json (#329)
Adds `version`, `indexPath`, and an ISO `lastIndexed` to `codegraph status --json`, plus a `CodeGraph.getLastIndexedAt()` library method. `agentCount` dropped (no clear consumer). Reworked from contributor PRs #333 and #480. Co-Authored-By: Javier Gómez <199902626+12122J@users.noreply.github.com> Co-Authored-By: Ran <8403607+eddieran@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c9559d9991 |
fix(watcher): bound fd/watch cost with a native fs.watch hybrid (#644, #496, #555, #628, #579) (#650)
chokidar v4 holds one OS file descriptor per watched file on macOS (libuv's kqueue backend registers an fd per vnode; fsevents is installed but v4 no longer uses it). On a large project the `serve --mcp` daemon accumulated tens of thousands of open REG descriptors and exhausted kern.maxfiles — crashing unrelated processes system-wide with ENFILE. #276 only trimmed the count by ignoring directories; the source tree still cost one fd per file. Replace chokidar with a pure-JS native fs.watch hybrid, keeping codegraph's zero-native-addon "any OS builds any bundle" invariant: - macOS / Windows: a single recursive fs.watch (one FSEvents stream / ReadDirectoryChangesW handle) -> O(1) descriptors regardless of repo size. - Linux: one inotify watch per directory (O(dirs), dynamic add for new dirs, capped via CODEGRAPH_MAX_DIR_WATCHES) instead of per-file watches. Validated empirically: macOS 0 extra fds at 6k and 12k files; Linux 31 inotify watches at 6k files (per-file would be 6k); Windows recursive catches nested and new-directory edits. Full test suite green. Tests drive the watcher through an inertForTests seam (no OS watcher) for determinism under parallel vitest, with one real-fs end-to-end test exercising the genuine native path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
68eaf0dbd8 |
feat(mcp): codegraph_explore as the sole primary tool + store coverage + overload disambiguation (#647)
## Summary
Completes the explore-overhaul arc: `codegraph_explore` becomes the single primary tool an agent reaches for, and its coverage + output shape are tuned so flow/architecture questions resolve with near-zero Read/Grep.
### What changed
- **explore is the sole primary tool** — removed `codegraph_context` (the fuzzy-input Read-trigger) and `codegraph_trace` (under-picked by agents); explore already surfaces the call flow among the symbols you name. A plain natural-language question now works as the query.
- **Store/handler coverage** — functions defined inside object literals (Zustand `create((set, get) => ({ … }))`, Redux/Pinia/MobX, exported handler/route maps) are indexed as real symbols, including calls through `useStore.getState().fn()` and destructured `const { fn } = useStore.getState()`. A general AST rule, not a per-lib hack.
- **Overload disambiguation** — explore leads with the *right* definition when a method name is overloaded across types (a PascalCase type token in the query biases to that type's own def); `codegraph_node` returns *every* overload's body in one call, with an optional `file`/`line` selector to pin one.
- **Method-atomic render** — explore never returns half a method; at the size budget it drops whole methods/files (and lists what it dropped) instead of truncating a body mid-method.
- **Native-read-shaped output** — per-call output is capped to ~24K with a 25K hard ceiling and concentrated into ~150–250-line flow windows, mirroring how the agent natively reads; repo size scales the *call* budget, not the per-call size (a larger response just gets externalized to a file the host Reads back).
- **Blast radius** folded into explore (dependents + covering tests, locations only).
### Benchmark (refreshed on this build)
Re-validated the 7-repo A/B on 2026-06-02 (Opus 4.8, effort=high, median of 4). WITH arm re-measured on this build, WITHOUT reused:
**~16% cheaper · 47% fewer tokens · 22% faster · 58% fewer tool calls** — 0 file reads on 6 of 7 repos (Gin ~1).
The arc trades larger, cache-heavy explore responses for guaranteed near-zero reads, so cost/token margins soften vs the prior build (Excalidraw and Tokio land at cost break-even) while time and tool-calls stay clear wins everywhere — consistent with the project's stated optimization target (latency + tool-calls, not token cost).
### Validation
- Full suite green: **1112 passed, 2 skipped**.
- 28/28 plain WITH runs across the 7 README repos completed clean; reads median 0 on 6/7.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
89d4d37a29 |
feat(npm): restore programmatic/embedded SDK API (#354) (#603)
The 0.9.x thin-installer turned @colbymchenry/codegraph into a bin-only
shim: require("@colbymchenry/codegraph") threw MODULE_NOT_FOUND and no
types shipped, breaking embedded library consumers (e.g. Electron apps)
upgrading from 0.8.0.
Restore programmatic use without re-bloating the thin shim or duplicating
the ~49 MB of grammars the per-platform bundle already carries:
- main -> npm-sdk.js re-exports the installed per-platform bundle's compiled
library (lib/dist/index.js) at runtime, reusing that bundle's own deps; it
falls back to a self-healed cache bundle, else throws an actionable error.
- types -> ship the .d.ts tree only (~590 KB) in the main package, built from
the same release so it can never skew from the runtime it re-exports.
- exports map resolves the `types` condition (nodenext) and the default entry.
- DatabaseConnection + QueryBuilder are now top-level exports, so embedded
callers get the building blocks from the package entry instead of deep
dist/ imports (which the shim no longer ships).
The CLI/MCP `bin` keeps execing the bundled Node; only library consumers run
on their own runtime, which must be Node 22.5+ for the built-in node:sqlite.
Validated end-to-end: built a real darwin-arm64 bundle, packed the npm
packages, installed them into a throwaway consumer, and confirmed require()
plus a full init/indexAll/searchNodes round-trip and the low-level
DatabaseConnection/QueryBuilder path all work on the host Node; types resolve
under both nodenext and classic node resolution; and the CLI shim still
launches. New hermetic tests cover npm-sdk resolution, cache fallback, and the
missing-bundle error.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
71935e37c2 |
feat(mcp): multi-module Go trace-quality + small-repo retrieval tuning (#494)
* feat(go): generated-file down-rank + gRPC stub-impl bridge + trace-failure inlining
Multi-pronged fix to make codegraph competitive on Go multi-module repos
(cosmos-sdk, etcd) where it previously lost or tied. Driven by an 8-question
agent-eval audit across cobra, gin, prometheus, cosmos-sdk, and etcd: the
baseline had codegraph losing ~60% on cost on cosmos-sdk and mixed on etcd
deep cross-module flows, while winning cleanly on the single-module and
non-protobuf-heavy repos.
Diagnostics ruled OUT `go.work` parsing as the gap (prometheus crushes
without it). The actual failure modes were generated-file noise warping
disambiguation, missing gRPC interface→impl bridge in structural-typing Go,
and trace's failure path triggering 3-5 follow-up tool calls instead of
inlining the material the agent needed.
Changes:
- New `src/extraction/generated-detection.ts` — path-pattern classifier
for `.pb.go`, `.pulsar.go`, `_grpc.pb.go`, `_mock.go`, `_mocks.go`,
`mock_*.go`, `.generated.[jt]sx?`, `_pb2(_grpc)?.py`, `.pb.{cc,h}`,
`.g.dart`, `.freezed.dart`. Applied as a stable sort tiebreaker in
`findSymbol`, `findAllSymbols`, `codegraph_search` (MCP + CLI),
`codegraph_explore` file ranking, and context formatter Entry Points /
Related Symbols / Code blocks. Cosmos's `msgServer.Send` now ranks #3
instead of #9 on a `Send` search.
- New `goGrpcStubImplEdges` synthesizer in `callback-synthesizer.ts` —
detects `UnimplementedXxxServer` structs in generated files, identifies
their RPC methods (excluding `mustEmbed*` / `testEmbeddedByValue` gRPC
markers), and emits `calls` edges to the matching methods on any
non-generated struct whose method-name set is a superset. Closes Go's
structural-typing gap that the existing `interfaceOverrideEdges` (Java /
Kotlin only) couldn't bridge. 467 bridge edges on cosmos-sdk; bank's
`UnimplementedMsgServer::Send` points to `x/bank/keeper/msg_server.go`
only, not to `msgClient` siblings or mock files.
- Trace-failure rewrite (`handleTrace`) — when no static path connects
endpoints, instead of telling the agent to call `codegraph_node` (a
3-4-call fan-out), inline both endpoints' bodies (120 lines / 3600 chars
per endpoint), their callers (≤6), and callees (≤8) in one response.
- Trace endpoint-pairing improvements — scores every `from`×`to`
candidate combo by shared directory prefix and tries the best-paired
pair first (the full candidate set, not just FTS top-5). A
less-canonical-path penalty (`enterprise/`, `contrib/`, `examples/`,
`vendor/`, `third_party/`, `deprecated/`, `legacy/`) ensures the
canonical-module pair wins even when a side-experiment shares more of
its directory prefix. Find-path probe budget capped at 20 pairs.
- Test-file deprioritization in `codegraph_explore` `isLowValue` — adds
suffix patterns (`_test.go`, `_spec.rb`, `.test.ts`, `.spec.tsx`,
`Test.java`, `Spec.kt`) alongside the existing directory-style patterns.
Otherwise etcd's `watchable_store_test.go` consumes 5K chars of explore
budget that should go to the hand-written flow source.
Tests:
- New `__tests__/generated-detection.test.ts` (4 unit tests) pins the
suffix patterns.
- New "Go gRPC stub→impl synthesis" integration test suite in
`frameworks-integration.test.ts` (2 tests): positive bridge from stub
to hand-written impl, AND the precision case (don't bridge to a
generated sibling like `msgClient` in the same .pb.go).
- Full suite: 1076/1076 pass.
Empirical (post-fix, n=2 average per question):
| Repo / Q | WITH | WITHOUT | Reads (W/WO) | Time (W/WO)
|-------------------------|------------|-------------|--------------|------------
| cobra (parse cmds) | $0.27 | $0.27 | 0 / 4 | 39s / 60s
| prometheus (scrape→TSDB)| $0.63 | $0.70 | 0 / 6 | 106s/143s
| cosmos-sdk Q1 (MsgSend) | $0.41 | $0.26 | 1 / 2 | 67s / 64s
| cosmos-sdk Q2 (Delegate)| $0.47 | $0.46 | 0 / 5 | 50s / 73s
| cosmos-sdk Q3 (gov tally)| $0.34 | $0.31 | 1.5 / 3 | 54s / 76s
| etcd Q1 (Put→raft) | $0.65 | $0.78 | 0 / 4 | 98s / 129s
| etcd Q2 (watch) | $0.36 | $0.50 | 0 / 4+ | 58s / 89s
Codegraph wins on reads + time on every question. Cost is mixed: 3 clean
wins, 3 tied (within 10%), 1 stubborn cost loss on the grep-favored Q1.
Compared to baseline, the cosmos-sdk cost-gap collapsed from -60% to -15%
on average, and Q3 went from a 75% loss to a tie. Raw run artifacts in
`/tmp/cg-finalv2-*/` and `/tmp/cg-final-*/`.
Memory written at `project_go_multi_module_audit.md` for the methodology
+ before/after numbers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): auto-inline trace in codegraph_context for flow queries
When a codegraph_context task contains a flow keyword ("trace", "from",
"reach", "flow", "propagat", "how does", "how do") AND at least two
distinct PascalCase / camelCase identifiers, internally invoke trace
between the first two extracted symbols and splice the trace body into
the context response. Conservative trigger by design: false positives
waste one graph query; false negatives just fall back to the agent
calling trace itself (existing path-proximity wiring handles either
case).
Goal: collapse the agent's typical context → trace → explore sequence
into a single context call for clear flow queries, closing the
remaining cost-overhead gap on multi-call patterns. The path-proximity
+ less-canonical-path scoring + the trace-failure-inlined-bodies
behavior already let the inline trace land on the right endpoint pair
and return enough material that no follow-up codegraph_node/Read is
needed.
Doesn't fire on:
- cobra's "How does cobra parse commands and flags?" (no PascalCase
symbols) — verified in regression run, no behavior change ($0.260
WITH vs $0.257 WITHOUT, basically tied)
- queries where the agent doesn't call codegraph_context at all
(cosmos Q1 in the audit went search → trace → node → trace → node)
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): trace failure inlines TO file siblings to displace node fan-out
The cosmos-Q1 audit revealed a static-resolution gap: msgServer.Send's
*real* next hop is `k.Keeper.SendCoins` — an interface-method call on an
embedded field that tree-sitter can't resolve. The static getCallees list
for msgServer.Send is all utility/error functions (StringToBytes, Wrapf,
…). The actual flow (SendCoins → subUnlockedCoins → addCoins →
setBalance) lives entirely inside `x/bank/keeper/send.go`, which is also
where the TO endpoint (setBalance) lives.
When trace fails (no static path), inline the **top 5 functions/methods
in the destination file**, ordered by line-distance from the TO node.
This catches the flow that interface-method calls obscure — the
canonical "k.<Iface>.<Method>" pattern in Go, also relevant to Java
dependency-injection / Rails service-object dispatch / etc. where
interface dispatch hides the real call.
Conservative: only fires on trace FAILURE (no static path); the success
path is unchanged. Per-body cap (40 lines / 1200 chars), top 5 siblings.
Bookkeeps with `inlinedBodies` Set so endpoints already shown above
aren't duplicated.
Result: cosmos-Q1 — historically the most stubborn cost loss (-2.2× to
-39% across the audit) — flipped to a clean WIN: $0.257 WITH vs $0.449
WITHOUT (-43%), 34s vs 79s, 0 Reads vs 2 Reads + 5 Greps, 5 codegraph
calls vs 12. Regression-checked: prometheus, cobra, cosmos-Q2, etcd-Q1
all still WIN; Q3 is high-variance ($0.30-$0.45 range historically) and
fell within that on this run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: extend coverage to all supported languages, not just Go
PR review feedback: the audit was Go-driven, so the patterns I added
were Go-flavored. Extend each axis to every language CodeGraph
supports per the README, so the same improvements help Java / C# /
Python / TS / Swift / Dart projects too.
**generated-detection.ts** — Added patterns for:
- TS/JS: `.gen.[jt]sx?`, `.pb.[jt]s`, `_pb.[jt]s`, `_grpc_pb.[jt]s`
(ts-proto, gRPC-web, Apollo / GraphQL codegen, Hasura).
- Python: `_pb2.pyi` (mypy stubs from protobuf).
- C#: `.g.cs` (T4 / Razor codegen), `Grpc.cs` (protoc-gen-csharp).
- Java: `OuterClass.java` (protoc-gen-java), `Grpc.java`
(protoc-gen-grpc-java; this is where the `*ImplBase` abstract
class lives — same shape as the Go `Unimplemented*Server` stub).
- Swift: `.pb.swift` (protoc-gen-swift).
- Dart: `.pb.dart`, `.pbgrpc.dart`, `.chopper.dart`.
- Rust: `.generated.rs`.
**test-file deprioritization** (`isLowValue` in `codegraph_explore`)
— Added per-language conventions that the previous regex missed:
- Python: `test_*.py` (pytest discovery) and `*_test.py`.
- Ruby: `*_test.rb` (minitest) — `*_spec.rb` already covered.
- C#: `*Tests.cs`, `*Test.cs`, `*Spec.cs`.
- Swift: `*Tests.swift` (XCTest).
- Dart: `*_test.dart`.
**IFACE_OVERRIDE_LANGS** in `callback-synthesizer.ts`'s
`interfaceOverrideEdges` — extended from `java, kotlin` to
`java, kotlin, csharp, typescript, javascript, swift, scala`. Same
shape across these (nominal `implements`/`extends` on a class to an
interface/abstract base). Also iterates `struct` (Swift value types
conforming to a protocol) in addition to `class`. The existing
matchesSymbol-style logic and `getOutgoingEdges(..., ['implements',
'extends'])` work unchanged.
**CLAUDE.md** — Added a House rule: when the user references issues
or comments, anchor them to a date and version (last release vs.
last main commit vs. current branch tip) BEFORE concluding a fix is
incomplete. Issue #388 comments from May 25-27 were responding to
the released v0.9.5 / merged-PR-469 state — not to this branch's
in-flight work. The new rule walks through the disambiguation:
`grep -m1 '^## \[' CHANGELOG.md` for release version, `git log
--first-parent main -1` for main tip.
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): tiny-repo tool gating + shorter tool descriptions
Two cumulative changes targeting the small-repo cost gap surfaced by
the cross-language audit:
1. **Tool descriptions trimmed** (~2.1KB total saved across 10 tools).
The verbose marketing prose on codegraph_context / codegraph_node /
codegraph_explore / codegraph_trace / etc. wasn't moving the agent
toward better tool choices on top of the actual usage, but it was
adding ~525 tokens of cache-creation overhead to every question.
The trimmed descriptions keep the operational hints (e.g. "Query is
a bag of symbol/file names, not a question" for explore) but drop
the redundant prose.
2. **Dynamic tiny-repo tool gating** in `ToolHandler.getTools()`. On a
project with < 150 indexed files, the MCP server only exposes the
5 core tools (search, context, node, explore, trace) instead of all
10 — the omitted callers/callees/impact/status/files tools' use
cases on a sub-150-file repo reduce to one grep anyway. The MCP
tool-defs overhead is the #1 source of cost loss on tiny repos
(~$0.10-0.15 fixed cache-creation per question); cutting 5 tools
drops that by ~50%.
Effect on ky (~25 files, the worst pre-fix offender):
- Before: $0.59 WITH vs $0.42 WITHOUT (+42% loss, n=1)
- After: $0.32 WITH vs $0.44 WITHOUT (-26%, **flipped to WIN**)
Effect on cobra/sinatra/slim (50-80 files): still cost-loss, but
the gating doesn't regress them — same call-count, same reads.
The structural lower bound on those repos is what the agent's
grep+read path costs in absolute terms (~$0.20-0.30).
Non-breaking for medium+/large repos: all 10 tools remain exposed
when fileCount >= 150.
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): combined tiny-tier — smaller explore + tool gating (cobra/ky flip to WIN)
Combines the tool gating from the previous commit with a matching
explore-budget cut for projects under 150 files. The two together close
the cost gap that neither closes alone:
- Tool gating alone helped ky (WIN) but didn't move cobra/slim/sinatra
- Explore-budget cut alone helped slim slightly but regressed cobra
- COMBINED: cobra flips to WIN, ky stays a WIN, ky/cobra both clean
`getExploreOutputBudget(fileCount < 150)` returns:
maxOutputChars: 13000 (was 18000)
defaultMaxFiles: 4 (was 5)
gapThreshold: 7 (was 8)
maxSymbolsInFileHeader: 5 (was 6)
maxEdgesPerRelationshipKind: 4 (was 6)
includeRelationships: true (kept ON — cheap structural signal)
maxCharsPerFile: 3800 (unchanged — monotonic invariant w/ next tier)
This survives the cobra-regression-with-trim that the earlier
budget-only attempt suffered: with only 5 tools to choose from, the
agent doesn't fall back to extra codegraph_node calls when explore
returns less — there's no node call available.
Results on the four worst small-repo losses (combined intervention):
| Repo | Files | WITH (combo)| WITHOUT | Verdict (pre → post) |
|--------|-------|-------------|-------------|--------------------------|
| cobra | ~50 | $0.25 | $0.31 | loss → **WIN** (-19%) |
| ky | ~25 | $0.39 | $0.39 | -42% → tied |
| slim | ~80 | $0.31 | $0.24 | LOSS 31% → still LOSS |
| sinatra| ~60 | $0.30 | $0.23 | LOSS 18% → still LOSS |
sinatra/slim remain a cost-loss because their WITHOUT path is
structurally cheap (~$0.20 — fewer than 4 cheap grep+read calls).
Codegraph can't beat that absolute floor with any meaningful response.
Both still WIN on time + reads + tool-call count.
Tests: tier boundary cases updated to cover the new <150 / 150-499 /
500-4999 / 5000-14999 / >=15000 progression. Off-by-one guard updated
to include the new 149↔150 boundary. All 1076 tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(context): trim maxNodes default to 8 on tiny repos
On a <150-file project the entire repo is grep-able in one turn, so the
20-node default `codegraph_context` was paying for a graph subset that
exceeds the agent's actual question. Cutting the tiny-repo default to 8
(typical 1-3 entry points + their immediate 1-hop neighbors) reduces
the context-tool response body without hitting sufficiency on the flow
shapes small repos actually contain.
Non-breaking: the agent can still pass an explicit `maxNodes` to
override; medium+ repos (>=150 files) keep the 20-node default.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(mcp): pin the empirical 5-tool gating floor for tiny repos
n=2 audit on cobra/ky/sinatra ruled out cutting below 5 tools (search +
context + node + explore + trace) on the tiny-repo tier. The smaller
3-tool gate (search + context + trace) saved ~$0.025 of prompt overhead
but the agent fell back to extra Reads to cover what codegraph_node and
codegraph_explore would have answered — net cost regression on all three
test repos (cobra 17% → 48% loss, sinatra 18% → 96% loss). Documented
inline so future tuners don't re-try this dead-end.
No behavior change beyond the comment: the 5-tool gate remains the
production setting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(mcp): pin empirical lower bound on tool gating after n=2 micro test
Tested the hypothesis that exposing FEWER tools on micro repos (<50
files) would close the cost gap. Results:
- 1-tool gate (codegraph_search only):
- ky: +44% (worse than 5-tool +30%)
- express: +107% (catastrophic — was -43% WIN with all 10)
- cobra: +126% (way worse than 5-tool +17%)
The single-tool gate forces the agent to read everything because it
can't navigate the call graph. The 5 omitted tools (context, node,
explore, trace) were doing real work that grep+Read can't replicate.
Conclusion: 5 tools (search + context + node + explore + trace) is the
empirical lower bound on the tiny-repo tier. Cutting below regresses
EVERY tested repo. The remaining ~$0.04-0.08 of structural cost overhead
on tiny repos is unavoidable without sacrificing the value codegraph
provides at that scale (which would also make WITH = WITHOUT, defeating
the install).
Comment documents the dead-ends so future tuners don't relitigate.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): iter3/iter4 — raise tool-gate to 500, sufficiency steering in context, hard-exclude low-value files
Three layered changes targeting the sinatra/slim/small-repo cost gap
that iter2's body-shrink failed to close (smaller bodies just pushed
the agent to Read instead):
1. **Tool-gate threshold 150 → 500** (`TINY_REPO_FILE_THRESHOLD`).
Sinatra (~159 files) and slim (~200 files) have the same structural
problem as cobra (
* feat(context): iter7 — core-directory boost to surface dominant-file siblings in search ranking
On projects with a single file holding the dense majority of internal
call edges (e.g. sinatra's `lib/sinatra/base.rb` at ~85% of in-file
edges), text search was favoring small focused extension files over the
core file. A small focused file like `multi_route.rb` wins on verbatim
name match + file-size normalization, burying the 1500-line core file's
longer method names (e.g. `route!` vs `route`).
Fix: detect the "dominant file" — the file whose in-file edge count is
≥3× the next candidate's — then add +25 to all results sharing its
directory prefix. This pulls the core file's siblings above
sibling-package extensions without hardcoding any repo structure.
`getDominantFile()` excludes test/spec files and generated files
(e.g. etcd's `rpc.pb.go` has 4× the in-file edges of `server.go` and
would otherwise hijack the boost toward generated protobuf stubs).
SQL pulls the top 20 candidates; path-pattern filtering handles what
SQLite LIKE can't express.
* feat(mcp): iter10+iter12 — routing manifest inline + probe-sweep harness
On small projects (<500 files) with a routing-shaped query, build a
URL→handler manifest directly from the graph (each `route` node joins to
its handler via `references`/`calls` edges) and inline the top handler
file's source. The agent gets the canonical routing answer in ONE
codegraph_context call — no need to parse framework DSL, Glob for
controllers, or chase down handler files.
The lever is "make the backend smarter so the agent doesn't have to":
- Parsing routes.rb / routes/api.php / urls.py DSL is the agent's job
in the WITHOUT arm. Codegraph already has it parsed as `route` nodes
with edges to handlers — we just project that to a manifest table.
- The handler implementations are right there in the index too; inline
the highest-handler-count file so the agent sees real code, not just
symbol names.
Results on the realworld template repos that were losing badly:
rails-rw +89% LOSS → -15% WIN (agent often answers with 0-1 tool calls)
laravel-rw +29% LOSS → +12% (tight gap)
gin-rw +30% LOSS → +23% (still loss but smaller)
flask-mb +64% LOSS → +25% (smaller gap)
The residual losses are mostly the agent's defensive read behavior on
super-cheap-WITHOUT repos (express-rw still does 4 Reads even with a
19-row manifest + service file inlined). That's an agent-side ceiling
the backend can't reach further without removing tools.
Also lands `scripts/agent-eval/probe-sweep.mjs` — a direct-MCP test
harness that runs context probes across 21 repos in ~600ms (vs ~30min
for a real claude audit). Enables rapid iteration on backend changes:
edit tools.ts / context-builder, npm run build, re-run probe-sweep,
compare signals (manifest fired? handler file inlined? response size?)
before paying for a claude run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): first tool call awaits catch-up sync (no stale rows for deleted files)
`MCPEngine.catchUpSync()` reconciles the index against the working tree
after open (catching `git pull`/`checkout`/`rebase` and any edits or
deletes made while no server was running). It was fire-and-forget — so a
tool call landing in the first ~50-300ms could race past it and serve
rows for files that no longer exist on disk. The per-file staleness
banner can't help here, because that signal is populated by the file
watcher (not by catch-up).
The fix: `catchUpSync()` now pushes its promise into `ToolHandler` via
`setCatchUpGate(p)`; the first `execute()` call awaits the gate and then
clears it. Subsequent calls pay nothing. Catch-up rejections are logged
by the engine and swallowed by the handler so a transient sync failure
never breaks tools.
Most visible on the "deleted everything between sessions" case, where
MCP previously returned stale rows pointing at non-existent files.
Validated end-to-end on a 10,640-file VS Code index: with the gate, a
codegraph_search for "ExtensionHost" against an empty (but stale-DB)
directory returns "No results found" after the catch-up drains the DB;
without the gate, the same call returns 10 stale hits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): cover small-repo retrieval tuning + auto-trace + iface-override expansion
Add entries for work that landed on this branch but wasn't yet in
[Unreleased]: tiny-repo tool gating + sufficiency steering + budget
tier, auto-inline trace in codegraph_context, routing manifest inline,
core-directory ranking boost, JVM-only interfaceOverrideEdges extended
to C#/TS/JS/Swift/Scala, and the shorter tool descriptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3808b4d0a8 |
fix(cli): include resolution + synthesizer edges in indexAll report (#413)
The orchestrator's per-file counter only sees extraction-phase edges, so the `X nodes, Y edges` line printed after `codegraph init -i` / `codegraph index` undercounts the graph — often by more than half on repos with heavy cross-file resolution (mall: 20 047 reported vs 45 629 actually in the DB). Snapshot (nodes, edges) before/after the full pipeline in `indexAll` and write the true delta back to the result. New lightweight `QueryBuilder.getNodeAndEdgeCount()` is one round-trip with no per-kind breakdowns. `indexFiles` (no resolution) and `sync` (uses `nodesUpdated`, not `nodesCreated`) are unaffected. Regression test added: `__tests__/integration/full-pipeline.test.ts > reports edgesCreated including resolution + synthesizer phases`. |
||
|
|
72c08c2bef |
fix(watcher): retain pending files on zero-result sync (#450)
* fix(watcher): retain pending files on zero-result sync
* refactor(watcher): detect lock-unavailable at the wrapper
Replace the heuristic `(filesChanged === 0 && durationMs === 0)` check
inside `FileWatcher.flush()` with a typed `LockUnavailableError` thrown
by `CodeGraph.watch()`'s sync wrapper. The wrapper has access to the
full `SyncResult`, including `filesChecked` — which is **only** zero
when `sync()` failed to acquire the cross-process file lock (a real
empty sync always has `filesChecked > 0` because `scanDirectory` ran).
That eliminates the heuristic's edge case where a fast no-op sync
returns `durationMs === 0` by `Date.now()` rounding and gets mistaken
for a lock failure on tiny projects.
The watcher's `catch` block now distinguishes `LockUnavailableError`
from real errors: it logs at `logDebug` (not `logWarn`) and does NOT
call `onSyncError` — so a long-running external indexer holding the
lock doesn't spam stderr every debounce cycle via the MCP daemon's
`Auto-sync error` handler. The existing post-catch path already
preserves `pendingFiles` and reschedules, so no new control flow is
needed.
A/B validated end-to-end against the built dist on macOS with a
three-scenario repro (lock held, lock released mid-flight, real sync
error):
- main: lock-held silently clears pendingFiles (BUG);
lock-released never recovers (no real sync runs).
- PR-as-is: lock-held preserves pendingFiles; lock-released
drains. Same observable behavior as wrapper-level.
- wrapper-level: same outcomes; lock-failure goes through the catch
path silently (logDebug only, no onSyncError noise);
real errors still surface via onSyncError.
Updates the regression test to throw `LockUnavailableError` (the real
contract surfaced to `FileWatcher` by `CodeGraph.watch()`), and
asserts `onSyncError` stays quiet during the lock-held cycle.
Closes #449.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8876defbc1 |
fix(nestjs): propagate RouterModule.register prefixes to controller routes (#459) (#460)
NestJS's RouterModule lets apps compose modular route prefixes across files
(`RouterModule.register([{ path: 'admin', module: AdminModule, children: [...] }])`
in `app.module.ts` sets the prefix for controllers declared in another file's
`@Controller()`). The per-file `extract()` only sees one file at a time, so a
`UsersController` indexed in isolation showed up as `GET /` instead of
`GET /admin/users`.
Add an optional cross-file `postExtract(context)` hook to FrameworkResolver,
called by the orchestrator once after each `indexAll` and after every
incremental `sync` that touched files. The nestjs implementation:
* walks every `*.module.{ts,js}` for `RouterModule.{register,forRoot,forChild}([...])`
and recursively resolves `children` into `Module → /full/prefix`,
* walks `@Module({ controllers: [...] })` for `Controller → Module`,
* matches each route node against its controller's class line range
(multi-controller files keep getting attributed correctly), and
* rewrites `name` while preserving `id` (route→handler edges intact) and
`qualifiedName` (still encodes the *original* in-file path, which keeps
the pass idempotent on a re-sync — `app.module.ts` edits propagate to
controllers in unchanged files without double-prefixing).
End-to-end validated against the exact reproduction in #459 (admin children
users) — all four routes (`GET /admin`, `GET /admin/users`,
`GET /admin/users/:id`, `POST /admin/users`) resolve correctly, edits to the
RouterModule tree re-propagate on the next sync, and route→handler edges in
`codegraph context` are preserved.
Closes #459
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|