Sync to upstream e63fe2e, and narrow the TS/JS patch layer to what upstream leaves - #18
Conversation
… index (colbymchenry#1431) The 1.6.0 notes said the write-ahead log is "capped", which reads as a limit on how much can be indexed. It bounds only the log's resting size (64 MB default, CODEGRAPH_WAL_HEAL_MB) and folds a killed session's leftover back into the index; a large repository's log still grows in proportion to its index while it is built. Say so in both entries, and document the two knobs in the README's troubleshooting section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MC52FSFLtKtDCLqT81tZYG
…pace (CG-40) Adds `ui/` as an npm workspace (Svelte 5.56 + Vite 7, devDependencies only — the engine's runtime dependencies are untouched) and chains its build into `npm run build`, so the browser viewer ships inside `dist/` with everything else: `build-bundle.sh` already copies `dist` wholesale and `pack-npm.sh` packs that bundle. Output is `dist/viewer/`, NOT `dist/ui/`: `src/ui/` is the engine's terminal ui (shimmer progress + its worker) and tsc compiles it to `dist/ui/`, so emitting there both deletes those modules — the CLI then dies at startup with `Cannot find module '../ui/shimmer-progress'` — and would leave the static server handing out compiled engine internals. The design spec is corrected to match. `scripts/check-ui-build.mjs` is the release guard: index.html must exist, be non-trivial, and every local asset it references must be on disk, and the compiled engine next door must still be intact. It runs after every UI build, again in `build-bundle.sh` once the bundle stage has copied `dist`, and again in `pack-npm.sh` once each archive is unpacked — so a broken viewer fails the release instead of shipping a CLI that serves a 404. `vite build` does not override an ambient NODE_ENV, so a shell or runner with NODE_ENV=development silently shipped dev-mode Svelte (~13 kB of dev-only runtime checks, warning in the user's console). The config now pins production for `command === 'build'`; macOS and Windows ARM64 then emit byte-identical bundle hashes. The shell itself follows docs/design/codegraph-ui-design-spec.md §2–§3.1: design tokens as CSS custom properties (light on bare `:root`, dark under both `prefers-color-scheme` and `[data-theme="dark"]`), square corners, hairline rules, one oxblood accent; top bar 48px / trail bar 34px / main; a hash router over `#/s/<id>`, `#/file/<path>`, with `#/map` and `#/flow` reserved for phase 2. Fonts are vendored through @fontsource rather than fetched, so a local reader works offline and never announces the project to a CDN. Verified: clean `npm run build` from an empty dist on macOS and on the Windows ARM64 VM (forward-slash asset URLs, CLI still starts, both assertion failure modes exit 1); `dist/viewer` present in a real darwin-arm64 bundle and in the packed npm platform package; shell geometry, tokens, all seven routes, both themes and font loading checked in headless Chromium with no console errors; `npm test` unaffected.
…d-only (CG-41) Adds the `codegraph ui [path]` command (alias `web`) and `src/ui-server/`, a `node:http` server with no framework and no new dependency. The command reads an index that already exists — it never creates one, so a missing index prints the same friendly guidance the MCP tools give instead of a stack trace, and a sensitive system directory is refused up front. Security is the substance here, not the routing. The server binds 127.0.0.1 only, answers GET and HEAD only, and sends no CORS headers ever. The realistic attack on a process that serves your source code from a local port is DNS rebinding, so every request must carry a loopback `Host` (on our port) and, if it carries an `Origin` at all, a loopback one — anything else is 403 before the filesystem is touched. Every path resolves through the engine's existing `validatePathWithinRoot` chokepoint, which already handles `../` traversal and in-tree symlinks pointing out of the root (colbymchenry#527); `..` segments are refused outright so a traversal attempt gets a 404 rather than the SPA shell. `PathRefusalError` moves from `mcp/tools.ts` into the dependency-free `errors.ts` (re-exported from its old home, so class identity and every `instanceof` check are unchanged) — that is what lets a non-MCP read sink enforce the same refusal without importing the MCP tool graph. Assets come from `dist/viewer/` resolved relative to `__dirname`, the way `db/index.ts` finds `schema.sql`. Hashed assets are cached immutably, `index.html` never. Port 4747, or the next free one — an explicit `--port` stays explicit rather than silently moving. `--no-open` skips the browser, and `CODEGRAPH_BROWSER` picks one (or `none` to suppress it), which is also what makes "did it open a browser" testable end to end. `resolveProjectFile` and the `/api/` handler seam are the boundary CG-42's JSON API plugs into; `/api/*` 404s as JSON so a typo'd endpoint never returns the app shell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CreateProcess — which node's spawn uses without a shell — only launches a real .exe, so a `.cmd`/`.bat` browser shim (how most Windows wrappers are written) silently launched nothing. Routing the override through `cmd /c`, the way the default `start` opener already goes, makes .exe, .cmd and .bat all work and keeps node's per-argument quoting so a path with spaces survives. Caught on the Windows VM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…console The 'no index' guidance carried a literal em dash and the banner an ellipsis. A Windows console on an OEM codepage decodes raw UTF-8 as mojibake (colbymchenry#168), which is exactly what getGlyphs() exists to avoid — seen on the VM. Guidance now takes its dash from the glyph set; the banner uses a plain '...'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
A CRLF file must come back with the graph's own line numbers and without a trailing carriage return on every line — the case a Windows checkout with core.autocrlf produces. It is decided by bytes rather than by the OS, so it is covered here rather than only on the VM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…red 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>
…G-44) A row's position comes from measuring the laid-out DOM, so between Svelte creating it and the first relayout it has no place to be. Drawing it at top: 0 stacks the whole rail at its head for a frame; keeping the previous symbol's coordinates is worse. It stays invisible until it has been placed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…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>
…heme (CG-43) The viewer's code block stops lexing with a hand-rolled dialect table and reads real TextMate grammars instead, run once in `/api/source`. Three things make that safe to depend on: * Highlighting never fails a request. A missing grammar, an oversized slice, an ESM import that did not resolve — every one of them answers `engine: 'plain'` with a reason and the source still goes out. * Identifiers survive whatever token boundaries a grammar chose. Every code token is split into identifier runs before it goes on the wire, so the graph's call-site overlay claims a token the highlighter produced rather than re-cutting the line. `assignRefs` now matches on a token's text rather than on the class a grammar gave it, so a language that scopes type names as `storage.type` still links. * The theme classifies rather than colours: its foregrounds are sentinels the server maps back to class names, and the viewer paints them from CSS custom properties — one token stream serves light and dark with no refetch, and the ramp lives only in app.css. Comments move from --ink-3 to a new --code-comment. --ink-3 measures 3.46:1 on paper and 3.00:1 on the hot-line tint, both under AA for 12.5px text; --code-comment is the smallest step along the same ramp that clears 4.5:1 on every background a code line can have, and stays quieter than the strings and numbers above it. Shipping: @shikijs/core and @shikijs/engine-javascript are runtime dependencies (no wasm, no native module); @shikijs/langs stays a devDependency and `npm run build:textmate` writes only the closure the engine's 40-odd languages reach — 56 grammars, 2.6 MB, against 11 MB for all 722. check-ui-build.mjs asserts the tree after every build and inside every release archive.
…ncy rails (CG-46) Clicking a file path now opens the file itself: what reaches into it, its symbols in source order, and what it reaches. The two rails count DEPENDENCIES, not import statements. The prototype drew `imports` edges; on this repo `src/graph/traversal.ts` imports two files and depends on four, because it reaches the LRU cache through a call no import names. A rail headed "Imports 2" would be quietly wrong about what changing the file would touch, which is the only question the screen answers — so the rails read `getFileDependencies` / `getFileDependents` and merge the import rows in for the symbol names. Imports that resolved to nothing indexed keep their own section rather than vanishing. The outline is windowed above 250 rows against a fixed 28px row: this repo's own fixtures hold a 1,681-symbol `.d.ts`, and paging it would hide the one thing an outline is for. `src/mcp/tools.ts` draws its 135 rows whole. `/api/file` gains `topLevel.calls` — module-level calls out of the file node — so a file that RUNS something offers the badge that opens it as a symbol, the only place code belonging to no symbol can be read. File results in the search palette and the entry-point list now land here rather than on the file node's Symbol view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (CG-47) A new user can now reach the viewer from the README alone: a "Read your graph in the browser" section with a screenshot re-shot from the real build, a step 5 in Get Started, a CLI Reference row, and the same content as a docs-site guide. - README: new section (what the three columns are, the options, the privacy posture), Contents entry, Get Started step 5, CLI row. Screenshot at assets/codegraph-ui-symbol-view.png, version-tagged ?v=1. - CHANGELOG: an [Unreleased] New Features entry in the user-facing voice. - codegraph help ui: mentions the `web` alias, says what the screen shows, and states that nothing is sent anywhere. - TELEMETRY.md: the viewer has no telemetry of its own and makes no outbound connections; the only thing recorded is the command name in the daily rollup, which every off-switch already suppresses. - site/: guides/viewer.md + sidebar entry, a `ui` section in the CLI reference, and a link from Next Steps.
…m 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>
…er hop (CG-50) Ask "how does execute reach getFile" in the search box and the viewer draws the call path between them, left to right, opening every card at the exact line that makes the next call. Dynamic-dispatch hops are dashed and name the site they were wired at; "Read as flow" turns a trail walked by hand into the same strip. The path finder is NOT new. `codegraph_explore` already leads its answers with the longest call chain among the symbols an agent named, and a viewer that drew a different path would get the two quoted against each other in a review. So the search moved out of `ToolHandler` into `src/graph/named-symbol-flow.ts` and both callers ride it — same tokens, same overload rules, same synthesized edges. What stayed behind in `tools.ts` is the prose. A pinned from/to question is the same search with two options changed, because both ends being named is the evidence explore's one-unnamed-bridge cap stands in for: it bridges freely, keeps twelve candidates per endpoint instead of six (the CLI's own `main` sorts seventh of ten), and searches from both ends at once — identical paths to the one-way walk on twelve measured pairs, 3-6x faster. `/api/flow` is deliberately the one endpoint with no cache: its cards carry source read from disk, and a drift verdict changes without the index changing. Verified on this repo (`execute` to `rowToFileRecord`, 8 hops; `main` to `resolveOne`, 7) and on a fresh excalidraw index, where `mutateElement` to `renderStaticScene` crosses callback, react-render and jsx-child hops and lists exactly the hops `codegraph_explore` prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le 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>
…he 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 (colbymchenry#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.
…nd cap (CG-51) A flow that does not reach what it was asked about now ends in a cap instead of in silence: the dispatch form that ended it, the line, the static key when the source spells one out, the candidate runtime targets as clickable rows, and the name-only matches under 0.6 the search refused to follow. A flow that does reach its destination never shows one. The verdict is lifted out of `ToolHandler` into `src/graph/dynamic-boundary-report.ts` and both callers render it — `codegraph_explore`'s prose and `/api/flow`'s `WireFlowBoundary` — the same move `named-symbol-flow.ts` made for the path finder, and for the same reason: a reader holding the strip and the MCP answer must not be told two different things. The explore prose is unchanged, byte for byte. When nothing connects at all and a dispatch site explains why, the strip is that site: one card opened at the line where the static path ends, plus the cap. When nothing explains it, no stopping point is invented.
…tarting points (CG-54) `#/entry` answers "where does anything start" at full length, and turns any row that names a symbol into a flow. Server. `/api/entrypoints` gains `frameworks` (from `getDetectedFrameworks`), a `tests` list, a `routes` limit of its own, and a cache keyed on the index build — nothing here is read from disk, so unlike `/api/source` a cached answer cannot be stale about drift. `routes.items` is now a `WireList` like every other list on the payload. Routes carry where the URL is REGISTERED as well as where it is served: `getRoutingManifest` selects the route node's id, file and line, and `buildRoutes` splits the verb off the name against a fixed list (never "the first word", which would take the head off a file-routed `/blog/[slug]`). All four payroll-go routes register in one router file and three are served from another — group by the handler file and one router becomes two groups plus an orphan. `isTestFile` is split into `isTestPath` (test filename and directory conventions) + the non-production catch-all, byte-identical at every existing call site. The Tests list uses the narrow half: an example, a benchmark or a fixture is off-target for ranking but is not a test, and a heading that says "Tests" must not quietly count them. Tests rank by REACH — distinct other files touched — because Go, Rust and Java put test work inside functions where a module-level-calls ranking sees nothing. Two read-only engine queries make that affordable: `getFileReachCounts` (the mirror of `getFileDependentCounts`, driven from `nodes` by path so the cost follows the files asked about rather than the edge table) and `getFileNodes`. Viewer. `ui/src/lib/entry-model.ts` folds the four lists into file groups — pure, and `panel.rows` stays exactly the sections it draws. `EntryView` + `EntrySection` render them with the caller rail's `.filegroup` / `.row` shapes rather than a second visual language for the same idea. A row that names a callable symbol carries a `Flow ›` chip; the other end is typed or picked with `→ here` on another row. File and test rows carry none: `/api/flow` searches by name, and a file has none the path finder can look up. A project with fewer than three resolvable routes gets no Routes heading at all, not an empty one. Typing into the search box now also returns matching entry points under their own heading below the symbol matches, so a URL comes back with its handler attached; rows already in the results are dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… README (CG-55) "Copy image" and "Download SVG" on the Flow strip's header and in the Map's side panel. The image is the distribution loop: a flow pasted into a review, a map pasted into a README, read by somebody with no viewer open. The exporter serialises the LAYOUT OBJECT rather than scraping the DOM — no html-to-image, no foreignObject, no new dependency. buildFlowLayout and buildMapLayout already compute every rectangle, port and curve before a component renders, so the image and the screen come from one piece of arithmetic and cannot drift apart, and the whole exporter is a pure function a test runs with no browser. Output is presentation-only SVG (rect, line, path, polygon, text, tspan, clipPath) — no script, no external reference, no data: URL — which is what GitHub's sanitiser accepts in a README. Light theme is forced whatever the viewer is set to: a dark strip on GitHub's white comment background reads as a mistake, not a preference. 24px of paper around the drawing, a caption naming the path or the root at the bottom left, a CodeGraph mark at the bottom right. Fonts travel as family stacks, not bytes (spec). An SVG loaded as an image may not fetch a webfont, so a raster falls back to the platform's own monospace — every fallback in the stack advances at ~0.6em like IBM Plex Mono, so the code grid survives and only the letterforms change. Text is truncated arithmetically with an ellipsis and clipped as well, so a wider fallback cannot spill a source line out of a card. `scale` multiplies only the root width/height while the viewBox stays in CSS pixels, so the raster draws an image whose intrinsic size is already 2x instead of upscaling a 1x bitmap. The clipboard write uses the ClipboardItem promise form (Safari discards the gesture across an await) and falls back to downloading the PNG, saying which happened rather than claiming a copy it did not make. Measured on this repo: execute -> rowToFileRecord (8 hops) exports 3690x253 CSS px, 491 kB PNG at 2x / 38 kB SVG; the 16-module map reproduces the canvas exactly — 16 boxes, 52 links, 9 layer rules, both band labels, and with src/index.ts selected 15 links and 4 dimmed boxes.
…ring Shiki (CG-57)
The viewer ran a second highlighter over source the engine had already parsed
with a real grammar: Shiki, plus 56 pruned TextMate grammars shipped in
dist/textmate/. The classification now comes off that tree instead, so a file is
read by exactly the grammar that decided what its symbols are.
The swap is complete rather than flagged: @shikijs/core, @shikijs/engine-javascript
and @shikijs/langs are off the dependency list, scripts/prune-grammars.mjs and
`npm run build:textmate` are deleted, and check-ui-build.mjs asserts the
tree-sitter grammars in dist/extraction/wasm instead of dist/textmate.
The wire contract is unchanged — `[classId, text]` pairs with the class names
alongside — so the viewer's decoder and code blocks did not have to be rewritten.
Two classes are added to the six: `type` (a named type reference, painted at
plain ink) and `def` (the name a definition declares, weight 600), the latter
taken from the extractors' own definition tables so it cannot drift from what
indexing calls a definition.
Three differences are not cosmetic:
* Interpolations (`${…}`, `#{…}`, `$"{…}"`, f-strings) are classified as code,
not as string. The call-site overlay refuses to claim a token classed string,
so calls written inside interpolated strings now link.
* Built-in type words are emitted whole and classed `type` in every language.
The grammars disagree about whether `string` is a type_identifier or an
anonymous token inside a predefined_type, and TextMate scoped them
inconsistently too.
* 3 000 lines of TypeScript cost 24-41 ms instead of ~700 ms.
Given up deliberately: Liquid, Razor, YAML, Twig, XML and .properties render
plain. .svelte/.vue/.astro are classified through their <script> blocks, the same
delegation the SFC extractors do. Pulling html/css/vue out of tree-sitter-wasms
would cover them, but those ABI-13 builds are the known cause of shared-WASM-heap
corruption for every other language in the same process.
Measured parity, per-language before/after screenshots and the reproduction
recipe: docs/design/cg57-highlighting-parity.md.
…one adapter (CG-61) `ui/src` now builds two ways from one tree: the static app `codegraph ui` serves, and — via `svelte-package` — a Svelte library the Pro app imports. A forked component would be a second answer to the same question about the same graph, so there is no fork. Everything a screen knows arrives through a `GraphAdapter`: eleven methods answering the wire shapes verbatim, with `createHttpAdapter()` (the loopback JSON API) as the default and a host's in-process engine reads as the point. `lib/api.ts` became a one-line-per-call facade over it, which is why no call site in the views changed. The payload types moved to `lib/wire.ts` — no imports, no runtime — so a host can depend on the vocabulary alone. Two more seams and one guard: - `lib/navigation.ts` holds the href builders behind a `NavigationDriver`, so a host addresses its own URL space. The app's half — the hash parser and the live route, which attach window listeners at module scope — stays in `router.svelte.ts` and is pruned out of the package: rendering a Symbol view must not install a hash router in somebody else's application. - `lib/theme.css` carries the design tokens and maps Svelte Flow's `--xy-*` variables onto them, so a host never sees library defaults. Dark now also answers to a bare `[data-theme]`, which is how `<CodegraphUi theme>` themes a container rather than the document. - `scripts/check-ui-package.mjs` prunes the app's shell, resolves the extensionless specifiers svelte-package leaves behind, and asserts that nothing but `lib/adapter.js` reaches the network. The search box, its keyboard and its panel are one component now (`SearchPalette`), because splitting them is what breaks a palette. `__tests__/ui-package.test.ts` mounts the three screens from the package entry against a mock adapter in jsdom; it runs as a second vitest project so the `browser` resolve condition it needs cannot reach the engine's suites. Versioned with the engine. Prepared, not published: `private: true` is the guard and `pack-npm.sh` only packs a tarball under CODEGRAPH_PACK_UI=1.
…atches through it (CG-58) A vertical tree above the members outline for classes, interfaces, structs, traits, protocols, enums, unions and type aliases: ancestors above (the whole chain, not just the direct parent), the focus in accent, subtypes below indented per level. `extends` draws solid, `implements` dashed; a synthesized edge — Go's implicit interface satisfaction — draws dashed wider and carries the site it was wired at, so a relation the resolver inferred never reads like one the source wrote down. For an interface the fan below IS the set of runtime targets a call can land on, and a type with eight or more implementers leads with that in a sentence. Members that redeclare an ancestor's are marked in the outline. The walk lives in `src/graph/type-hierarchy.ts`, following CG-50/CG-51: shared computation in `src/graph/`, presentation in the caller. Its `countImplementers` is now also what `ToolHandler.buildPolymorphicBoundaries` counts with, so "N types implement X" is the same N whether an agent reads it or a person does. `/api/node` carries the block as `hierarchy` rather than a second endpoint — it is part of the Symbol view's first paint, and gated to types, so a function costs one kind test. Layout is arithmetic (24px rows, 22px indent, orthogonal connectors computed from the two): no ResizeObserver, same payload → same picture. The header's `extends X` / `implemented by …` chips are suppressed while the tree is on screen — two renderings of one relation in one column is how a reader ends up trusting neither. `TypeHierarchy` is exported from `@colbymchenry/codegraph-ui` and takes its data as a prop, so a host holding a `WireSymbolPayload` renders it without a second read.
…g that could still reach it (CG-59) A Dead code screen and a mark on the Map, both drawn from one derivation in src/graph/dead-code.ts so a second surface can never disagree with the first. The SQL half is four lines — no incoming edge but `contains`. It returns ~2 500 candidates on this repository and the shipped list is 20; everything in between is the feature. A candidate is dropped the moment there is any reason to believe something outside the graph reaches it: exported symbols and header declarations, test and generated files, abstract and interface members, anything carrying a `decorates` edge, overrides of an ancestor's member, names the language calls by itself, vendored directories, files nothing in the index reaches (those are islands, and the Map says so instead), names the resolver failed to resolve somewhere, and names shared with a symbol that IS referenced — the mis-resolution that leaves a used method with a self-edge and its twin with nothing. The last rule is the only one that is not a graph query: before a claim is made, the declaring file and every file that reaches it are read and the identifier counted, which is what catches the references the extractor never recorded (`this.handleMessage.bind(this)`, a call inside an object literal, a shorthand property). Every subtraction is counted and printed under the list with the scale it came from, and the caveat line above it never collapses: the claim is "no static reference in the index", not "unused". On the Map a module nothing depends on keeps its stroke and says so in its count line, and tool-generated files and modules recede to ink-4 there, in the map's file list, in search results and on the file screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on is (CG-59) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a re-index (CG-60) Save trail on the trail bar writes the walk to .codegraph/ui/trails/ as one JSON file, listed on the empty screen and on Entry points above the derived suggestions, reopened at the symbol you left with the whole path restored. A hop is stored by qualified name, kind and file — never by node id, which contains a start line and so changes the first time anybody edits above the symbol. Every hop is re-resolved against the current index on the way out and each row says what became of it: still here, moved to another file, now ambiguous, or gone. A hole is never stitched over: the row opens the longest run of CONSECUTIVE resolved hops and says which ones those are, because the trail is a path and a skipped hop would draw a call that does not exist. This is the first write the viewer makes, and the boundary moved with it: POST/DELETE answer under /api/ only, must carry X-CodeGraph-UI and application/json (neither of which a cross-origin form can produce without a preflight this server answers none of), and --read-only refuses both while still listing what is there. The blanket "read-only" claim is retired from the banner, the README, the CLI help and the docs site in favour of the narrower true one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three epics, 20 tasks, landing as one subsystem. CG-39 (phase 1, the reader): loopback-only read-only server behind `codegraph ui`, a read-only JSON API over the index, the Symbol view (callers | gutter-ported source | line-anchored callee rail), the search palette and trail, and the File view. CG-48 (phase 2, the map and the flow): the module-granularity Map, the Flow strip over one shared path finder, the "where the graph stops" end cap, the whole-file source view with intra-file call arcs, live refresh over SSE, the entry-points panel, and SVG/PNG export. CG-56 (phase 3, depth and a library): syntax classification taken off the engine's own tree-sitter parse (retiring Shiki and its 56 bundled grammars), the type hierarchy, dead code and islands, saved trails, and ui/ packaged as @colbymchenry/codegraph-ui. Two derivations were lifted out of ToolHandler into src/graph/ so the viewer and codegraph_explore can never draw different answers from the same graph: named-symbol-flow.ts and dynamic-boundary-report.ts. Saved trails are the viewer's only write. The loopback boundary gained a write shape (POST/DELETE under /api/ carrying x-codegraph-ui, no CORS headers ever) rather than being widened; `--read-only` turns it off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps.
Introduce Expo Router integration by adding a new Screens view and API to surface screens and their transitions. Implement a map-based layout with directional ports, extended layering and port pitch to accommodate edge labels, and a pill-based labeling system for transition conditions. Include tests for the new map/screens models, updates to the UI components, and design/docs changes reflecting the Screens design. Merge CodeGraph UI viewer changes to render and interact with Expo Router-based screen graphs. This enables CodeGraph UI to surface screens and navigations from Expo Router apps.
…s and introduce Steps API Introduce Expo Router integration with a new Screens view and API to surface screens and transitions, plus a new Steps API and UI to depict typed steps from anchors or symbols. Extend codegraph’s extraction and resolution to handle namespace objects (export default NAME, two-statement forms, and default bindings) and React hook bindings for handlers, improving accuracy of flows across JS ↔ native boundaries. Add Swift/React Native bridge receiver evidence (RCT_EXTERN_MODULE, RCT_EXTERN_METHOD) and related resolution logic, with tests covering namespace-object resolution, useCallback-driven handlers, and inline RN event listeners. Update UI to include a Steps tab and associated components (StepsView, StepNode, ScreenEdge) and wire navigation to expose steps-based exploration via /api/steps and UI routes. Documentation and changelog reflect the new Expo Router integration and steps surface capabilities.
colbymchenry#2038) * fix(db): guard synthesis metadata and migrate the expression index Bisect identifies 2a2f71e (colbymchenry#2033) as the malformed JSON regression. Guard index expressions, ownership predicates, wiring lookups, and the legacy Go backfill; schema v11 replaces existing v10 indexes. Cover malformed base edges, staged publication, upgrade replay, and indexed range lookup. * fix(db): preserve call precedence in deterministic edge traversal Independent bisect identifies 2a2f71e (colbymchenry#2033): source-first ordering puts importing file IDs ahead of function callers and consumes the default caller limit. Restore kind-first incoming and outgoing traversal with deterministic endpoint/position ties. Keep both original regression tests unchanged and cover filtered, cached, and provenance-filtered queries across reverse insertion order. Validation: 25 test files passed (564 tests, 3 skipped), including security, MCP caller truncation, sync, migration, graph, and DB/query coverage; npx tsc --noEmit passed. Both bisects rebuilt tsc and copied assets at every step with CODEGRAPH_KERNEL=0 to avoid using the HEAD kernel on historical extraction code. Final validation used the normal kernel setting. * chore: no changelog entry for an unreleased regression Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…timer delay (colbymchenry#1966) (colbymchenry#2039) Node runs a setTimeout delay above 2^31-1 ms after 1 ms instead, so a huge CODEGRAPH_WATCHDOG_TIMEOUT_MS SIGKILLed a healthy server at once and a huge CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS abandoned the launch within milliseconds. Both parsers now cap at 2147483647. (cherry picked from commit a242f57) Co-authored-by: Eric Minish <eric.minish@gmail.com>
…mchenry#2040) Bisect identifies 5eaa6fe (colbymchenry#2034, colbymchenry#1820) as the first bad commit: its Python named-import fallback resolves task.delay() to the task function itself, hiding the queue effect in API steps. Require member resolution before returning a named Python import's target for attribute access. Keep direct calls, callbacks, constructors, and known members working. Add absolute/relative aliased-import regression coverage without changing the existing steps assertions. Validation: git bisect with preserved steps test, tsc and copy-assets at every step, CODEGRAPH_KERNEL=0 throughout bisect. 338 tests pass across ui-steps-api-servers, function-ref, rebuilt kernel-tsjs-parity, resolution, and ui-effects. Issue colbymchenry#1820 repro and tsc --noEmit pass.
…lbymchenry#2041) Workers whose project open failed were enrolled as idle and returned errors indefinitely. Route failed readiness through the existing idempotent retirement and replacement path. Ignore retired-worker messages and duplicate readiness handshakes while preserving the crash budget and startup limits. Cover mixed pools, repeated failures, lifecycle duplicates, and real SQLite recovery. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…bymchenry#1963) (colbymchenry#2042) * fix(mcp): close pending sockets and allow read-only fallback (colbymchenry#1963) * fix(mcp): log when a fallback serves reads without auto-sync (colbymchenry#1963) A read-only fallback starts no watcher and holds no writer lock, so a session that quietly stopped syncing looked the same as a healthy one in the logs. Name the holder on stderr when the fallback goes read-only. Suggested by @bompus in colbymchenry#1979. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): stop pending connections and serve read-only fallbacks (colbymchenry#1963) Pending handshakes could block daemon shutdown or register phantom sessions after disconnect. Track accepted sockets and reject late hello continuations. Use explicit read-only engines and SQLite connections for blocked fallbacks, suppressing synchronization, watching, migrations, and repairs. Preserve writer ownership and the existing project lifecycle, with regression coverage for colbymchenry#1963 and colbymchenry#1356. Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…y#1959) (colbymchenry#2043) * fix(lock): keep a live writer lock regardless of file age (colbymchenry#1959) * fix(watcher): re-arm after lock contention and reconcile before trusting index (colbymchenry#1959) * feat(mcp): report exact index freshness without blocking the daemon (colbymchenry#1959) * fix(mcp): refuse drifted explore source after watcher degradation (colbymchenry#1959) * fix(mcp): refuse graph answers that name a drifted file after watcher degradation (colbymchenry#1959) Explore already refuses to render source from a file that changed while auto-sync was off. search, callers, callees and impact answer from the same frozen graph but kept replying, naming the changed file as if its edges were current. They now take the same refusal: the indexed files an answer names as locations are hashed against the index, and a drifted one is named instead of the answer. codegraph_node keeps its own drift gate, which serves the changed file's current bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): recover index freshness after lock contention (colbymchenry#1959) Keep a live writer's lock regardless of its age. Re-arm lock-degraded watchers with throttling and retain stale state until full reconciliation succeeds. Validate contributing files through structured paths and hashes, refusing incomplete checks without tool errors. Report bounded freshness measurements and cover Git fallback exclusions. Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…colbymchenry#2044) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… alive (colbymchenry#2045) The live-holder case wrote `pid: 1` and leaned on its own comment, "On Linux, PID 1 is almost always alive". Windows has no PID 1, so tryAcquireWriterLock read the holder as dead and acquired — the correct behaviour for a dead holder, and a failing assertion. The platform-specific part was the fixture, not the lock. A parked child process is alive on every platform, and not stealing from a live holder is what the lock actually promises, so the case now tests the promise rather than a number that happens to name a running process. The child is reaped in afterEach. Fixes colbymchenry#1752. (cherry picked from commit bee1402) Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
…nry#1779) (colbymchenry#2046) A vitest pool worker is the one launch path that never received `--liftoff-only`, so the suite compiled tree-sitter grammars on the turboshaft tier. Once ~640 extraction tests have warmed a grammar function up, its background tier-up job (Turboshaft LoopUnrollingPhase, symbolized from the native stack) exhausts a compiler Zone and aborts the worker: `Fatal process out of memory: Zone`, which vitest reports only as "Worker exited unexpectedly" with the file's remaining tests unrun. Pass WASM_RUNTIME_FLAGS through poolOptions.forks.execArgv, so the test process matches the bundled launcher, the CLI re-exec and refresh-launcher. V8 flags are process-global, so parse worker threads are covered too. Linux x64, Node 24.15, `__tests__/extraction.test.ts` (655 tests): without the flag 3 of 4 runs died at the same test (639 passed); with it 2 of 2 passed, in 19–24s instead of 34–41s. Neither `--no-wasm-loop-unrolling` nor `--no-wasm-dynamic-tiering` prevents it. (cherry picked from commit 565ec81) Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…lbymchenry#2047) Ship an extensionless shell launcher alongside codegraph.cmd for Windows. Use bundled node.exe with matching runtime flags and preserve arguments, exit status, and inherited host PID. Validate on Windows 11, macOS, and Linux, including CLI/hooks and archive contents. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#1294) (colbymchenry#2048) Recognize MINGW, MSYS, and Cygwin and print the exact PowerShell installer command. Keep Darwin/Linux behavior unchanged and exit non-zero before downloading. Cover shell dispatch and real Windows ARM64 Git Bash behavior. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) (colbymchenry#2049) * fix(mcp): arm liveness watchdog in local proxy * fix(mcp): terminate wedged local proxies with watchdog (colbymchenry#943) Install the independent liveness watchdog in the local handshake proxy and stop it during shutdown. Build on PR colbymchenry#979 while preserving existing daemon assertions. Add process-termination, slow fallback, and watchdog cleanup coverage. Validate the repro and targeted tests on Windows, macOS, and Linux. Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…1451) (colbymchenry#2050) * fix Windows watcher false edits from NTFS atime * fix(watcher): ignore Windows access-only notifications (colbymchenry#1451) Check indexed size and mtime before recording Windows watcher edits. Preserve scope reconciliation and fail open when metadata cannot be verified. Add native Windows regression coverage and validate on Windows, macOS, and Linux. Co-authored-by: JJordan0K <69581081+JJordan0C@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: JJordan0K <69581081+JJordan0C@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…y#1325) (colbymchenry#2051) * fix: safely stop daemon before reindex * fix(index): coordinate rebuilds with running MCP daemons (colbymchenry#1325) Stop verified project daemons and confirm termination before recreating SQLite. Fence daemon and proxy reopening throughout rebuilds while preserving foreign and newer locks. Add Windows lifecycle coverage and cross-platform shutdown regressions. Co-authored-by: Harry B ✦ <drewavesx0@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): fence project opens during index rebuilds after rebase Carry the colbymchenry#1325 rebuild fence through the upstream explicit-project lifecycle, read-only engine opens, synchronous initialization retries, and direct writer acquisition. Preserve shared project leases, read-only fallback behavior, freshness handling, and direct-mode query pools. Cover watched, unwatched, and read-only engines blocking during a foreign rebuild and recovering after the fence is released. * fix(mcp): answer a running index rebuild as guidance, not a tool error (colbymchenry#1325) A projectPath call during `codegraph index` hit assertNoRebuild's plain Error and returned isError; it is an expected, temporary condition, so it now uses a RebuildInProgressError that the tool handler answers success-shaped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Harry B ✦ <drewavesx0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: close resolution graphs before removing Windows fixtures Close ArkTS and JVM graph connections in afterEach, including failed assertions. Verified both files on Windows 11 and macOS (32 tests). * test: compare explore source lines across LF and CRLF checkouts Normalize only line endings when matching emitted source against checked-out fixtures. Preserve the closure coverage and whole-line assertions. Both files pass on Windows 11 and macOS (15 tests). * test: exercise old Git fallback without a POSIX shell shim Inject only the unsupported ls-files invocation and run every other Git command against real repositories. Assert that each rejected call retries, use portable graph paths, and clean up fixtures. Passes on Windows 11 and macOS. * test: normalize Python fixture before generating CRLF cases The tests added in colbymchenry#2004 produced CRCRLF from a Windows checkout. Normalize the input to LF before constructing either variant. All 28 native and WASM cases pass on Windows 11 and macOS. * test: allow time for eviction fixture indexes on Windows The colbymchenry#2036 eviction test builds and reconciles nine real indexes and consistently exceeds its default five-second timeout on the VM. Allow 15 seconds for this test alone, preserving every assertion. The full file passes on Windows 11 and macOS (21 tests). * test: finish MCP processes before resetting Windows fixtures Launch actual servers with their runtime flags and await termination before directory removal. Use asynchronous removal retries for daemon fixtures. Establish a ready daemon and stop its first proxy before simulating PID reuse, then make source stale without opening a competing SQLite writer. Preserve lock and database byte-equality assertions. All touched files validated on Windows 11 and macOS. * test: allow Windows named-pipe fallback polling to finish The VM takes up to 25.7 seconds to complete the nominal six-second retry loop. Allow 30 seconds for the three fallback waits and 40 seconds for the two shorter enclosing tests, retaining their lock-preservation and read-only assertions. The full daemon file is validated on Windows 11 and macOS. * fix(daemon): retry transient Windows PID-file sharing violations A transient EPERM replacing daemon.pid aborted startup, observed in the quiet-client test and reproduced with a real locked handle at ba3c21e. Retry only Windows sharing errors with five bounded delays, rechecking ownership each time and cleaning temporary files. Keep publication synchronous before client processing. Cover transient/permanent failures, changed ownership, non-retryable errors, and a real Windows handle; require a confirmed daemon round-trip before measuring a quiet session. Validated on Windows 11 and macOS.
colbymchenry#2054) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e pair (colbymchenry#2055) The inheritance target-kind gate (colbymchenry#1796) rejected any extends/implements resolution whose target cannot be a supertype. VS Code declares every service twice under one name, `export const IFoo = createDecorator<IFoo>(…)` beside `export interface IFoo`, and the import resolver takes the first export of that name, which is the value. The gate then dropped the edge instead of taking the interface the same file declares. Found by the pre-release v1.6.0 vs main comparison: 864 `implements` edges lost on vscode (AccessibilityService -> IAccessibilityService and ~860 like it), plus the ~5.8k interface-dispatch call edges synthesized from them. The gate now moves a TypeScript constant/variable target to the one same-named supertype in its file; with no such sibling it still drops the edge. vscode: implements 4,139 -> 5,003 (v1.6.0: 5,436, 254 of them on non-type targets; now 0), node count unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mchenry#2057) The pre-release agent A/B (main vs v1.6.0, 7 README repos, 3 runs each) had main returning 15% less source on vscode for the same queries, with Reads in 2 of 3 runs where v1.6.0 had none. Replaying every query both builds' agents issued traced it to three causes: 1. Named gaps (colbymchenry#1711) displaced source. Every cluster fit measured the gap names, which on a long path run to hundreds of chars, so a class shrunk into its file's room overran it by the names alone and was dropped whole: rpcProtocol.ts emitted 222 of 5,496 funded chars (v1.6.0: 5,194), and the test fixture came back as an empty fence. Fits now measure bare gaps; names are added at assembly from what the file's budget has left. 2. Interface members (colbymchenry#1638) corroborated English words. The NL-stopword guard lets a bare word seed a definition when another query word names a symbol in the same file; `readonly host` in an options interface made "main" seed `main()` in agentHostServerMain.ts, which took the named-first tier from the answer files. Members declared inside an interface no longer count as corroboration. 3. The interface-dispatch note announced dispatch through any common base, declared or not: "`extension` -> runtime dispatch to 2706 types implementing Disposable" opened 9 of 31 vscode answers. A supertype now has to declare the member (it, an ancestor, or a same-named Swift extension). When absence can't be judged, because a protocol's requirements aren't indexed, the note stays as before. Replayed queries, source lines vs v1.6.0 (main -> fixed): vscode 10,744 -> 12,577 (12,617), excalidraw 13,065 -> 13,698 (13,764), django 8,420 -> 8,803 (8,954), tokio 13,618 -> 14,151 (14,600), okhttp 6,511 -> 6,883 (7,090), alamofire 7,019 -> 7,249 (7,120); no query below 60% of v1.6.0. Real dispatch notes (Alamofire adapt/retry/asURLRequest, django, okhttp) are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…colbymchenry#2056) `open() itself kicks off the heal` polled the WAL size for 30s and then asserted it. On a loaded machine the heal was still running when the deadline passed, so a slow heal failed as a size assertion, reading like a regression (colbymchenry#1773). Await the pass open() started instead: it still fails at once if open() stops kicking off the heal, and reports the heal's own result rather than the clock's. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…thing binds Upstream now names a function passed through a curried wrapper after the binding its result lands in (colbymchenry#1747, PR colbymchenry#2002): the declarator, or an object member's key. That binding is the identifier call sites use. The merge left both mechanisms in place and the fork's ran first, so every wrapped function kept its wrapper string as its name and upstream's naming never applied. wrappedFunctionName, on both extraction paths, now returns nothing when curriedWrapperBoundName names the function, and the wrapper string names only a wrapper whose result nothing binds: an argument to another call, an array element, an arrow body, a return value. On sst/opencode that is 38 of the 1,055 wrapper sites; upstream alone reaches the other 1,017. The four cases the fork had added to upstream's extraction.test.ts and kernel-tsjs-parity.test.ts asserted the old naming. They go, so both files are upstream's again, and fm-agent-wrapper-named-functions.test.ts takes their place with one source holding every shape: the two bound ones keep upstream's names, the six unbound ones get the wrapper string, and both extraction paths agree.
…bare-name bridge
Under upstream's naming a service member is a node named `evaluate`, and the
namespace survives only in its wrapper string, `Effect.fn("Policy.evaluate")`.
The service-local matcher looked members up as `Ns.method` node names, so it
found nothing, and about 470 calls on sst/opencode went unresolved:
`const policy = yield* Policy.Service; policy.evaluate()`, and direct calls on
an imported namespace such as `EventV2.readAggregate()`.
effectWrapperMembers reads the wrapper string off the member's own line, or
the line above when the function starts on the next one. The namespace comes
from the receiver's own declaration as before, or from a capitalised receiver
itself; the calling file must import it, and only a unique member resolves.
The bare-name bridge goes. It linked `helper()` to a node named `Ns.helper`,
for a binding the old naming had hidden. The binding is the node's name now,
and all the bridge still did was mint an edge whenever a string's tail matched
another name: `const invoke = Effect.fn("Service.run")(…)` made every bare
`run()` a call to it. The pre-filter escape in index.ts existed for these
lookups and changes nothing without them, so index.ts is upstream's again.
The merge brought in PR colbymchenry#1865, which this patch was kept byte-identical to, so dart.ts and dart.rs already match upstream. Its regression cover goes with it; upstream's dart-extension-type.test.ts, from the same pull request, pins the same shapes.
FORK.md records the new base and the four patches it leaves: the work-directory exclusion, wrapper naming where nothing binds the result, Effect service member resolution, and generator class fields. It also corrects the generator entry, which said the kernel already listed `generator_function`: upstream's kernel does not, and the fork's patch changes both extraction paths. The CHANGELOG's fork entries are rewritten to match. The one for the chained-receiver fix goes: that patch was reverted in favour of upstream's colbymchenry#1759 in the previous sync, but its entry had stayed.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Verification before merge 1. Does this branch still fix what #12, #15 and the since-reverted chained-call fix were for? The input code from the #12 and #15 tests, taken verbatim, indexed with upstream
Run as-is, the #15 test file has 6 failures, all asserting that a node is named after the wrapper string (such as 2.
Where the real-run edges come from: opencode's own OTLP export, with a scripted fake LLM driving 10 session scenarios, and each Effect span's parent/child pair turned into a function → function edge. The roughly 1,000 edges 3. Known regressions against
Overall this branch is not worse than |
Moves the base from
3ed73bcto upstreame63fe2e(main, 2026-09-27), 93commits on (91 of them not merges), and releases as
1.6.0-fmagent.6.Supersedes #17, which was never merged: this branch is built on it, so its
commits come along, and the fork layer is then cut back to what upstream still
leaves.
Upstream has since merged four things this matters for:
extension typemembers stopped being indexed in #1780 — themethod_signaturegate has no language check colbymchenry/codegraph#1784). Patch 5here was kept byte-identical to it, so it drops out.
passed through a curried wrapper is named after the binding its result lands
in — a declarator, or an object member's key.
and fix(rust): keep self method calls on the correct owner colbymchenry/codegraph#1882 for Rust: self.method() resolves to a same-named method on an unrelated type colbymchenry/codegraph#1861 (Rust
self.method()owners), both reported from here.What happens to each patch
1.
fm_agentwork-directory exclusion — unchanged.2. Wrapper naming — narrowed to what nothing binds. After the merge both
mechanisms were in place and the fork's ran first, so every wrapped function kept
its wrapper string as its name and colbymchenry#2002 never applied. Now the fork defers:
wrappedFunctionNamereturns nothing wherevercurriedWrapperBoundNamenames thefunction, on both extraction paths. The wrapper string names only a wrapper whose
result nothing binds:
On
sst/opencodethat is 38 of the 1,055 wrapper sites — arguments to anothercall, array elements, arrow bodies, return values. Upstream alone reaches the
other 1,017.
3. Effect-TS resolution — the bare-name bridge goes, the service matcher is
rebuilt on upstream's names.
The bridge linked
helper()to a node namedNs.helper, for a binding the oldnaming had hidden. With the binding now the node's name, all it still did was
mint an edge whenever a string's tail matched another name. This is the codex
P2 on #17:
invoke()run().5invokeService.run(false)invokerunThe service matcher looked members up by
Ns.methodnode names, which no longerexist: under upstream's naming the member is
evaluate, and the namespacesurvives only in
Effect.fn("Policy.evaluate")on its own line. Without achange, about 470 calls on
sst/opencodego unresolved —const policy = yield* Policy.Service; policy.evaluate(), and direct calls on animported namespace such as
EventV2.readAggregate().effectWrapperMembersnowreads the wrapper string off the member's line; the namespace comes from the
receiver's own declaration, or from a capitalised receiver itself; the calling
file must import it; only a unique member resolves.
The pre-filter escape in
src/resolution/index.tsexisted for these lookups andchanges nothing without them — identical call edges on
sst/opencodewith andwithout it — so that file is upstream's again.
4. Generator class fields — unchanged. Upstream's generator fix (colbymchenry#1741) still
does not reach the two class-field helpers, on either extraction path. FORK.md
said the kernel already listed
generator_function; it does not upstream, andthe patch changes both paths, so that entry is corrected.
5. Dart extension types — dropped, being colbymchenry#1865.
The four cases the fork had added inside upstream's
extraction.test.tsandkernel-tsjs-parity.test.tsasserted the old naming. They go, so both files areupstream's again, and their cover moves into
fm-agent-*files. Against upstreamthe fork now touches 21 files, down from 25.
Verification
sst/opencodeatdf23b7f, indexed with each build and read through FM-Agent'sown TypeScript consumer (
src/languages/typescript.py):.5e63fe2eEffect.fn*wrapper sites with a function nodeThe 5% fewer edges are upstream's, and mostly false ones it stopped minting.
Comparing function-to-function edges outside test code by source position, 1,281
of
.5's are absent here. About 1,170 of them are method calls on a receiver,and in the samples we read nearly all are a value of an unrelated type landing on
a same-named project function —
value.split("/"),lines.at(-1),path.normalize(p),window.location.assign(href)— which upstream now declineswithout receiver evidence. Of the service calls the old
naming reached, all but 9 still resolve.
Tests, with
GIT_CONFIG_GLOBAL=/dev/nulland a locally built kernel(
CODEGRAPH_KERNEL_EXPECT=1):e63fe2eThe four extra files are the fork's
fm-agent-*suites. Run against upstreamunpatched, exactly the nine cases that cover patches 2 and 3 fail, and the rest
of those two files pass.
Not fixed here
In a monorepo where two packages each map
@/*to their ownsrc, upstreame63fe2eresolves an@/import in one package against the other's. Onsst/opencode,import { showToast } from "@/utils/toast"inpackages/appbinds to the same-named handler in
packages/opencode; 17 call sites move to awrong target this way. The
.5build, on3ed73bc, does not do this. It is upstream's, not thefork's, and is left for an upstream report.
Release
1.6.0-fmagent.6. FM-Agent pins.1today; moving its pin is a separate changethere once this is released.