diff --git a/AGENTS.md b/AGENTS.md index e307c7cc..cb601df9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,21 @@ a second positional argument to a research command. If the index is unavailable, follow `status` recovery guidance; do not initialize or rebuild unless requested or required by that guidance. +The committed viewer bundle in `crates/codegraph-ui/viewer/` is minified +`make ui` output. Indexed, it adds about 3,300 symbols, nine in ten of them with +one- or two-letter names, that crowd the most-depended-on and entry-point lists, +and every name it shares with real code lowers that name's exact-match +confidence. Keep it out of a local index with `.codegraph/config.toml`, which is +local and never committed; `codegraph sync .` (or a running daemon) applies it: + +```toml +[app] +name = "codegraph-rust" + +[indexing] +exclude = ["crates/codegraph-ui/viewer/"] +``` + ## Hard invariants 1. **Deterministic graph output.** Identical source and configuration must produce diff --git a/docs/upstream-sync/UPSTREAM.md b/docs/upstream-sync/UPSTREAM.md index f29c605a..9252942f 100644 --- a/docs/upstream-sync/UPSTREAM.md +++ b/docs/upstream-sync/UPSTREAM.md @@ -148,6 +148,16 @@ below remain immutable historical evidence. ## Sync log +### 2026-10-03 — `ui/README.md` rewritten for this repository + +The imported frontend README described upstream's npm monorepo: its build, the +`@colbymchenry/codegraph-ui` package and its publishing, and engine files under +`src/`. It now describes this repository: `make ui` and `make ui-check`, the +committed bundle the crate embeds, the Rust files behind the server and the +engine, and what the Rust server does not send yet. Its frontend-design sections +are upstream's, with their paths, numbers and claims checked against this tree. +A later sync of upstream `ui/` keeps this README rather than taking upstream's. + ### 2026-10-03 — browser viewer RESTYLED: direction D, a local design layer The ported viewer's frontend now carries the design the owner selected diff --git a/ui/README.md b/ui/README.md index e1168e33..b8328d27 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,90 +1,98 @@ -# ui/ — the `codegraph ui` viewer, and `@colbymchenry/codegraph-ui` +# ui/ — the frontend of `codegraph ui` -> **In codegraph-rs.** This tree was imported from upstream colbymchenry/codegraph -> `v1.6.1` and is built here by `make ui` into `crates/codegraph-ui/viewer/`, -> which the Rust binary embeds — see [`../docs/ui.md`](../docs/ui.md), the -> canonical reference. Its look is direction D -> ([`../docs/design/viewer-d.md`](../docs/design/viewer-d.md)) with a dark and a -> light theme. The build, packaging and release passages below describe -> upstream's npm monorepo, not this repository. +The browser reader for an indexed project: Svelte 5 + Vite, built into +`crates/codegraph-ui/viewer/`, embedded in the `codegraph` binary and served +over loopback by `codegraph ui`. [`../docs/ui.md`](../docs/ui.md) is the +reference for the command, its JSON API, its boundary and what it does +differently from upstream; this file is about the frontend itself. -One source tree, two builds. +The tree was imported from upstream colbymchenry/codegraph `v1.6.1` (MIT, see +[`LICENSE`](LICENSE)) and keeps upstream's formatting, so a later upstream sync +can still diff it. Two things are local layers on top, and +[`../docs/upstream-sync/UPSTREAM.md`](../docs/upstream-sync/UPSTREAM.md) records +both for that sync to re-apply: -- **The app** — the browser reader for an indexed project: Svelte 5 + Vite, - built as static files into `../dist/viewer` and served by the CLI over - loopback. -- **The library** — the same components, packaged with `svelte-package` into - `dist/` as `@colbymchenry/codegraph-ui`, so a host (CodeGraph Pro) renders - the Symbol view, the Flow strip and the Map over its **own** graph reads. +- the look: direction D + ([`../docs/design/viewer-d.md`](../docs/design/viewer-d.md)), with a dark and + a light theme; +- this README. -They are one tree on purpose. A forked component is a second answer to the same -question about the same graph, and sooner or later the two get quoted against -each other in a review. - -An npm workspace of the engine, so `npm ci` at the repo root installs the -toolchain for both. - -Design spec (every token, size and measurement): upstream's -`docs/design/codegraph-ui-design-spec.md`; in codegraph-rs, -[`../docs/design/viewer-d.md`](../docs/design/viewer-d.md). +Upstream also packages these components as `@colbymchenry/codegraph-ui` from +`src/index.ts`. That entry is kept, but this repository builds only the app. ## Build +The toolchain is pinned in `package.json` and `package-lock.json`, and `npm ci` +is the only install path. From the repository root: + ```bash -npm run build # from the repo root: tsc -> copy-assets -> this app -npm run build:ui # just the app, plus the dist assertion -npm run build:lib # the LIBRARY: svelte-package -> ui/dist, plus its checks -npm run dev -w ui # Vite dev server on 127.0.0.1:5174 -npm run check -w ui # svelte-check +make ui # npm ci, then vite build into crates/codegraph-ui/viewer/ +make ui-check # npm ci, svelte-check, vitest, a fresh build, and a byte check ``` -`build:lib` is deliberately not part of `npm run build`: the CLI does not need -it, and a release that fails because a component library would not compile is a -release that failed for the wrong reason. - -`npm run build` emits **`dist/viewer/`** (`index.html` + hashed assets). -`scripts/check-ui-build.mjs` then asserts the tree is complete, so a broken UI -build fails the release instead of shipping a CLI that serves a 404. The same -check runs again in `scripts/build-bundle.sh` (after the bundle stage copies -`dist`) and in `scripts/pack-npm.sh` (after each archive is unpacked). - -### Why `dist/viewer` and not `dist/ui` - -`src/ui/` is the engine's **terminal** UI (shimmer progress and its worker) and -tsc compiles it to `dist/ui/`. Pointing Vite there deletes those modules — the -CLI then dies at startup with `Cannot find module '../ui/shimmer-progress'` — -and would also leave the static server handing out compiled engine internals. -`check-ui-build.mjs` re-asserts the compiled engine is intact after every UI -build so that mistake cannot land twice. - -## `@colbymchenry/codegraph-ui` - -```svelte - - - - - +Inside `ui/`: + +```bash +npm run check # svelte-check +npm test # vitest: the suites in tests/ +npm run dev # Vite on 127.0.0.1:5174, with no API behind it ``` -Exports: `SymbolView`, `FlowStrip`, `ArchitectureMap`, `FileView`, -`FileSourceView`, `EntryPointsView`, `DeadCodeView`, `TypeHierarchy`, `TrailBar`, -`SavedTrails`, `SearchPalette`, `PalettePanel`, `PaletteRows`, `DriftBanner`, -`KindGlyph`, `ExportButtons`, `CodegraphUi` — plus every pure model function the screens are -built from (`buildCalleeRail`, `buildFlowLayout`, `buildMapLayout`, -`buildHierarchyModel`, `tokensByLine`, …) and the `Wire*` types an adapter -answers in. +The bundle is committed. The crate's `build.rs` embeds every file under +`crates/codegraph-ui/viewer/`, so `cargo build` and `cargo install --git` never +need Node. `make ui-check` is the CI `UI` job and part of `make pre-ci`. It +fails when the committed bundle is not byte-for-byte a fresh build, so a change +to `src/` lands together with the bundle `make ui` rebuilt from it. -`TypeHierarchy` is the one screen that takes its data as a prop rather than -asking the adapter: it is part of `SymbolView`'s payload (`/api/node`'s -`hierarchy`), so a host that already holds a `WireSymbolPayload` can render the -tree on its own without a second read. +`npm run dev` serves the app on its own, so every `/api/…` request it makes +fails. To work against a real index, add a `server.proxy` for `/api` to your +local `vite.config.ts` that points at a running +`CODEGRAPH_UI=1 codegraph ui --no-open --port `. Or rebuild with `make ui` +and run the binary. -### The adapter is the only way data arrives +## Layout + +``` +src/ + main.ts fonts, the stored theme, mounts App into index.html's #app + index.ts upstream's library entry; kept, not built here + app.css the shell grid and the primitives (islands, pills, buttons, …) + lib/theme.css the design tokens: Daylight (light) and Nebula (dark) + lib/theme-choice.svelte.ts System / Dark / Light, stored and applied as data-theme + lib/icons.ts Lucide icon geometry (ISC), drawn as CSS masks by Icon + lib/adapter.ts GraphAdapter, createHttpAdapter, the registry + lib/wire.ts every Wire* payload shape — types only, no runtime + lib/api.ts the screens' calls, one line each, over the adapter + lib/navigation.ts href builders + navigate, behind a driver + lib/destinations.ts what the nav rail, the phone tab bar and the More sheet offer + App.svelte nav rail / command bar / trail ribbon / main, global keys + lib/router.svelte.ts hash router: #/s/, #/file/, #/map, #/flow, #/entry, … + lib/trail.svelte.ts the walked path; mirrored into the `t` query param + lib/trails.svelte.ts saved trails: one shared fetch, and the two writes + lib/trails-model.ts what a saved trail's row says, incl. its decay (pure) + lib/kinds.ts kind glyph letters + lib/symbol-model.ts the Symbol view's rails and callee rows (pure) + lib/map-model.ts the Map's deterministic layered layout (pure) + lib/flow-model.ts the Flow strip's card/link geometry + the end cap — a DAG (pure) + lib/filecode-model.ts the whole-file view: fixed line height, arcs, paging (pure) + lib/hierarchy-model.ts the type hierarchy's tree geometry (pure) + lib/entry-model.ts the entry-points panel: rows, file groups, flow arming (pure) + lib/screens-model.ts the Screens view: layering by distance from the entry, edge labels, pill lanes (pure) + lib/export-svg.ts the Flow strip and the Map as a standalone SVG (pure) + lib/export-image.ts rasterising that SVG to PNG, clipboard and download + lib/live.svelte.ts /api/events: two counters every screen refreshes from + lib/toast.svelte.ts the one transient note ("Index updated · reloaded") + components/ NavRail, TopBar (the command bar), TrailBar, PhoneTabBar, MoreSheet, ThemeSwitch, SearchPalette, SavedTrails, KindGlyph, Icon, DriftBanner, ErrorCard, Toast, ExportButtons, map/, flow/, symbol/, file/, entry/, screens/, steps/ + views/ one component per route +tests/ the vitest suites, one per model or behaviour +``` + +Fonts (Inter and JetBrains Mono, both variable) are vendored through +`@fontsource-variable` and emitted with the bundle's assets — never inlined, so +the server's `font-src 'self'` policy holds: a local reader must work offline +and must not announce the project to a font CDN. + +## The adapter is the only way data arrives ```ts interface GraphAdapter { @@ -97,46 +105,55 @@ interface GraphAdapter { fileCode(path, signal?): Promise; flow(request, signal?): Promise; map(request?, signal?): Promise; + screens(signal?): Promise; routes(request?, signal?): Promise; entryPoints(request?, signal?): Promise; deadCode(request?, signal?): Promise; trails(signal?): Promise; - // The only mutating pair, and the only optional methods besides `events`. + // Optional. + steps?(request, signal?): Promise; saveTrail?(request, signal?): Promise; deleteTrail?(id, signal?): Promise; - events?(handlers): () => void; // optional: the live channel + events?(handlers): () => void; // the live channel } ``` -The shapes are exactly what `src/ui-server/api/` serialises, and they live in -`src/lib/wire.ts` — no imports, no runtime — so a host can depend on the -vocabulary without depending on the viewer. The default implementation, -`createHttpAdapter()`, is the loopback JSON API; a host that already holds the -index implements the same thirteen required methods against its own reads and -never makes an HTTP request. `scripts/check-ui-package.mjs` asserts that no module in the -built package but `lib/adapter.js` touches the network, because a screen that -reached past the adapter would be a screen that ignored the host. +The shapes are what `crates/codegraph-ui/src/api/` serialises, upstream's wire +format kept as it is, and they live in `src/lib/wire.ts`, which has no imports +and no runtime. The app runs every screen on `createHttpAdapter()`, the loopback +JSON API. + +`steps` is optional, and `createHttpAdapter()` leaves it out, because the Rust +server does not build Steps yet. The Steps view therefore says it cannot draw +steps. `events` is optional. Omit it and nothing connects and nothing polls; a host that learns about a sync some other way calls `live.signal('index')` instead, which is the same code path the stream uses. `saveTrail` / `deleteTrail` are optional for a different reason: they are the -only methods in the interface that CHANGE anything, and a host must be able to -render the reader without inheriting a write it never asked for. Omit them and -`TrailBar` grows no Save button and `SavedTrails` says the host does not store -them — the same thing it does when `trails()` answers `readOnly: true`, which is -how a host that *can* store them declines a particular project. `trails()` itself -is required: a host with nowhere to keep them answers an empty read-only list, so -the screen is explained rather than silently missing. +only methods in the interface that CHANGE anything, and a reader must be able to +render without inheriting a write it never asked for. Omit them and `TrailBar` +grows no Save button and `SavedTrails` says the host does not store them — the +same thing it does when `trails()` answers `readOnly: true`, which is what +`codegraph ui --read-only` does. `trails()` itself is required: a host with +nowhere to keep them answers an empty read-only list, so the screen is +explained rather than silently missing. + +`TypeHierarchy` is the one screen that takes its data as a prop rather than +asking the adapter: it is part of `SymbolView`'s payload (`/api/node`'s +`hierarchy`), so whoever already holds a `WireSymbolPayload` can render the tree +without a second read. ### Three things that will bite -1. **Import `theme.css` once.** Every component paints from the design tokens. - Override any variable on a narrower selector — including on a container, - since custom properties inherit; `` uses exactly - that to put a light reader inside a dark application. +1. **The tokens are the only colours.** Every component paints from + `lib/theme.css`. Daylight sits on the bare `:root`; Nebula sits under + `prefers-color-scheme: dark` and `[data-theme='dark']`. Both sets use the + same names, so a new colour is a token in both. `tests/theme-contrast.test.ts` + fails when a declared text/surface pair drops under 4.5:1, when the copies of + a set drift apart, or when the SVG export's palettes stop matching them. 2. **The adapter and the navigation driver are module-level, not context.** The pure model modules are plain TypeScript and cannot read a component's context, so one page reads one project. `` installs them during @@ -144,7 +161,7 @@ the screen is explained rather than silently missing. (`{#key project}`), not swapping the prop. 3. **Geometry is not themable.** 44px callee rows, the 288/320px rails, the 20px code line: the Symbol view measures these against each other to put a callee - row beside the line that calls it. Colour and type are yours. + row beside the line that calls it. ### Navigation @@ -156,58 +173,8 @@ because middle-click, cmd-click and "copy link address" are how people read code. The app's half — parsing the hash, holding the live route — is -`src/lib/router.svelte.ts`, which attaches `hashchange`/`popstate` listeners at -module scope and is therefore **pruned out of the published package**. Nothing a -host imports may drag a hash router into its application. - -### Versioning and publishing - -The package is versioned with the engine (`scripts/sync-ui-version.mjs` runs on -every `build:lib`): `@colbymchenry/codegraph-ui@X.Y.Z` is the reader for -`codegraph@X.Y.Z`, because the payload shapes are versioned with the binary that -serves them. - -It is **prepared, not published.** `"private": true` in `package.json` is the -guard — npm refuses to publish it — and `scripts/pack-npm.sh` only builds the -tarball when `CODEGRAPH_PACK_UI=1`, into `release/npm-ui/` (never -`release/npm/`, whose `codegraph-*` glob the release workflow publishes). -Publishing is the maintainer's call and takes two deliberate edits. - -## Layout - -``` -src/ - index.ts the LIBRARY's entry — everything the package exports - main.ts fonts + tokens, mounts App into index.html's #app - app.css the app's reset, shell grid and primitives - lib/theme.css the design tokens (light/dark) + the Svelte Flow map - lib/adapter.ts GraphAdapter, createHttpAdapter, the registry - lib/wire.ts every Wire* payload shape — types only, no runtime - lib/api.ts the screens' calls, one line each, over the adapter - lib/navigation.ts href builders + navigate, behind a driver - App.svelte top bar / trail bar / main, global keys - lib/router.svelte.ts hash router: #/s/, #/file/, #/map, #/flow, #/entry - lib/trail.svelte.ts the walked path; mirrored into the `t` query param - lib/trails.svelte.ts saved trails: one shared fetch, and the two writes - lib/trails-model.ts what a saved trail's row says, incl. its decay (pure) - lib/kinds.ts kind glyph letters - lib/map-model.ts the Map's deterministic layered layout (pure) - lib/flow-model.ts the Flow strip's card/link geometry + the end cap — a DAG (pure) - lib/filecode-model.ts the whole-file view: fixed line height, arcs, paging (pure) - lib/entry-model.ts the entry-points panel: rows, file groups, flow arming (pure) - lib/screens-model.ts the Screens view: layering by distance from the entry, edge labels, pill lanes (pure) - lib/export-svg.ts the Flow strip and the Map as a standalone SVG (pure) - lib/export-image.ts rasterising that SVG to PNG, clipboard and download - lib/live.svelte.ts /api/events: two counters every screen refreshes from - lib/toast.svelte.ts the one transient note ("Index updated · reloaded") - components/ NavRail, TopBar (the command bar), TrailBar, PhoneTabBar, MoreSheet, SavedTrails, KindGlyph, Icon, DriftBanner, ErrorCard, Toast, ExportButtons, map/, flow/, symbol/, file/, entry/, screens/ - views/ one component per route -``` - -Fonts (Inter and JetBrains Mono, both variable) are vendored through -`@fontsource-variable` and emitted with the bundle's assets — never inlined, so -the server's `font-src 'self'` policy holds: a local reader must work offline -and must not announce the project to a font CDN. +`src/lib/router.svelte.ts`. It attaches `hashchange`/`popstate` listeners at +module scope, which is why upstream keeps it out of the component library. ## Export @@ -234,7 +201,7 @@ Mono, so the code grid survives and only the letterforms change. | hash | view | |---|---| -| `#/` | nothing selected | +| `#/` | Start: the project and what its index holds | | `#/s/?hl=&t=` | symbol view | | `#/file/?hl=` | file view — outline in source order | | `#/file/?src=1` | file view — the whole file's source, with ports and call arcs | @@ -243,7 +210,9 @@ Mono, so the code grid survives and only the letterforms change. | `#/flow?symbols=a,b,c` | flow strip — `codegraph_explore`'s own question | | `#/flow?t=` | flow strip — the trail you walked, read as a flow | | `#/entry` | entry points — routes, files that run something, tests, hubs | +| `#/dead?exported=1` | dead code — `exported=1` widens it to exported symbols | | `#/screens` | screens — the app's screens and the transitions between them | +| `#/steps?anchor=&symbol=&depth=&through=1&view=order` | steps — what happens from a screen or a symbol | ## Entry points @@ -271,11 +240,14 @@ draws, the same identity the search palette rests its keyboard on. `#/screens` draws `/api/screens` — the app as its user meets it: one box per screen, an arrow for every way of getting from one to another, and on each -arrow the condition under which it happens. The canvas is the Map's layout -engine (`buildMapLayout`) driven by `screens-model.ts` with three options the -Map never sets, because a screens graph differs from a module graph in one way -that shapes the whole picture: it is full of cycles. Every screen returns to -Home. +arrow the condition under which it happens. This server answers upstream's "no +screen navigation" result until the navigation resolvers are ported, so here +the canvas is exercised by its suite rather than by an index. + +The canvas is the Map's layout engine (`buildMapLayout`) driven by +`screens-model.ts` with three options the Map never sets, because a screens +graph differs from a module graph in one way that shapes the whole picture: it +is full of cycles. Every screen returns to Home. - **Layering is distance from the entry screen**, measured over every transition — not longest path over a two-cycle-broken set. Shared chrome (a @@ -289,7 +261,7 @@ Home. so it is drawn around the boxes rather than through them; a transition between two screens on one row arches over the row, top to top. A hub widens so its ports are at least 12px apart (`portPitch`), and rows are 116px apart - instead of the Map's 74 (`layerGap`), because the edges here carry labels. + instead of the Map's 44 (`layerGap`), because the edges here carry labels. - **Lines fan out instead of stacking.** Drawn through one midpoint, every line between two rows crosses that height at its middle, and a line to a screen far to the side is nearly horizontal there — a hub's lines run stacked @@ -323,7 +295,7 @@ Home. The geometry — ports, curves, pill lanes — is arithmetic in `screens-model.ts` and `map-model.ts`, tested without a browser in -`__tests__/ui-screens-model.test.ts`. +`tests/ui-screens-model.test.ts`. ## Where the graph stops @@ -343,25 +315,26 @@ Two rules hold it together: same place ran out for the same reason, and two caps side by side would read as two different findings. -The verdict itself is not computed here or in the server: it is -`findDynamicBoundaries` in `src/graph/dynamic-boundary-report.ts`, the same -detector `codegraph_explore` announces boundaries with. +The verdict itself is not computed here. The server's `find_dynamic_boundaries` +(`crates/codegraph-ui/src/api/boundary.rs`) runs `scan_dynamic_dispatch` from +`codegraph_mcp::dynamic_boundaries`, the same detector `codegraph_explore` +announces boundaries with. ## The type hierarchy A class, interface, struct, trait or enum carries a `hierarchy` on its `/api/node` payload: ancestors up, subtypes down, and the fan an interface call dispatches into. `buildHierarchyModel` turns it into a tree whose geometry is -arithmetic — 24px rows, 22px of indent per descendant level, orthogonal 1px -connectors computed from those two numbers. Nothing is measured; the same -payload always draws the same picture. +arithmetic — 26px rows (`HIER_ROW_H`), 28px of indent per descendant level +(`HIER_INDENT`), connectors computed from those two numbers. Nothing is +measured; the same payload always draws the same picture. The details worth knowing before changing it: - **`extends` is solid, `implements` dashed `4 3`, a synthesized edge dashed - `6 3`** with a `via ` pill. In Go, `System` satisfies `Clock` - without either file naming the other and the edge exists only because the - resolver made it — the block says so rather than drawing it like a parse. + `6 3`** with a `via ` pill. The Rust index synthesizes no + dynamic-dispatch edges, so this server never sends that last kind, and a Go + interface's implicit implementations are not part of its tree. - **Overrides on the members outline are a NAME match**, not an `overrides` edge (nothing in the engine emits one). They are matched against the nearest ancestor that declares the name and are blind to signatures, and the tooltip @@ -370,10 +343,10 @@ The details worth knowing before changing it: reader looking at an interface gets every direct implementation before any subclass of one appears at all. -The walk is not computed here or in the server: it is `buildTypeHierarchy` in -`src/graph/type-hierarchy.ts`, whose `countImplementers` is also the number -`codegraph_explore` prints when it announces an interface dispatch — so "N types -implement X" is the same N wherever you read it. +The walk is not computed here: it is `build_type_hierarchy` in +`crates/codegraph-graph/src/hierarchy.rs`. Upstream's `countImplementers`, which +upstream's `codegraph_explore` prints when it announces an interface dispatch, +is not ported. ## Live updates @@ -394,11 +367,11 @@ counter into a single call; the Map and the Flow strip instead read Reconnection is ours, not `EventSource`'s: each failure closes the stream and schedules ONE retry on a backoff that ends after eight attempts (~90 s), at -which point the top bar says "Not live" and nothing more is requested until the -tab is focused again. A `degraded` event — the server's watcher gave up — is +which point the command bar says "Not live" and nothing more is requested until +the tab is focused again. A `degraded` event — the server's watcher gave up — is shown the same way and never answered with a poll. Node ids and file paths are encoded per slash-separated segment, so -`#/file/src/mcp/tools.ts` stays readable and still round-trips a segment -containing a reserved character. Build hashes with `symbolHref()` / +`#/file/crates/codegraph-ui/src/api/mod.rs` stays readable and still round-trips +a segment containing a reserved character. Build hashes with `symbolHref()` / `fileHref()` / `mapHref()` / `flowHref()` rather than by hand.