Skip to content

feat(ui): serve the upstream browser viewer - #298

Merged
sunerpy merged 7 commits into
mainfrom
feat/ui-viewer
Oct 2, 2026
Merged

sunerpy merged 7 commits into
mainfrom
feat/ui-viewer

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1, part A of the browser viewer (UI family, upstream v1.6.1): a Rust port of upstream's loopback viewer server and the codegraph ui command, serving upstream's own ui/ frontend unchanged apart from one line. The look stays upstream's here. The direction D restyle and the light theme follow in the stacked PR-B.

  • codegraph-ui crate (new). An axum server on 127.0.0.1 that answers upstream's viewer API from an existing index: /api, stats, search, node, nodes, source, file, filecode, routes, entrypoints, map, deadcode, flow, screens, events (SSE) and trails. Wire shapes, limits, error codes ({error, code, hint?}, 400/403/404/503/500) and refusals all follow upstream. Every request opens the published index read-only through Store::open_for_read and drops it when it answers. The bundle under crates/codegraph-ui/viewer/ is embedded by build.rs, so cargo install --git still needs no Node.

  • Boundary, ported with its tests:

    • loopback bind;
    • Host and Origin allowlists (the DNS-rebinding guard);
    • writes need x-codegraph-ui: 1 and a JSON body type, and are capped at 64 KB, measured on the bytes that actually arrive;
    • raw-path .., control-byte, backslash and bad-escape refusals;
    • one containment chokepoint for every file read;
    • CSP, nosniff and X-Frame-Options: DENY; no CORS headers.
  • Engine ports the routes need, all SELECT-only:

    • dead-code report, named-symbol flow (named and directed modes), flow continuations, symbol lookup, type hierarchy, ICU-order collation (codegraph-graph);
    • aggregate viewer queries (codegraph-store);
    • additive WatchOptions::observe_only (codegraph-watch). It swaps the watcher's two sync closures for reporters, so /api/events reuses the real watcher (per-platform registration, symlink mapping, topology escalation) and never writes the index.
  • CLI. codegraph ui [path] [--port N] [--no-open] [--read-only], alias web. It is gated by CODEGRAPH_UI=1 before argument parsing, exactly as upstream gates it, and stays hidden from --help unless enabled. The banner, port fallback (4747 plus 20) and CODEGRAPH_BROWSER match upstream, and SIGINT/SIGTERM shut it down.

  • Trails, the viewer's one write, go to <index root>/ui/trails/<slug>.json. The temp file is renamed into place atomically, so a CODEGRAPH_DIR override moves trails with the index. Each hop is re-resolved on read (ok / moved / ambiguous / missing). --read-only refuses saves with 403.

  • Frontend. ui/ is imported verbatim at v1.6.1 in its own commit; that commit's tree equals v1.6.1:ui (7e3b4400…). A separate commit adds the build and test harness:

    • pins upstream's lock and adds a standalone package-lock.json;
    • runs upstream's 17 frontend suites under vitest;
    • removes steps from the HTTP adapter (see below).
  • CI. A new UI job runs make ui-check, which:

    • runs npm ci, svelte-check, vitest and vite build;
    • fails unless the committed bundle is byte-for-byte that build, untracked assets included.

    CI Success now needs it.

  • Docs.

    • docs/ui.md (new) and docs/design/viewer-d.md (the selected Penpot spec, plus §3.6 Daylight).
    • docs/cli.md, docs/architecture.md, docs/README.md, AGENTS.md, and the README entry in both languages.
    • An UPSTREAM.md entry with the re-audit (the latest stable tag is still v1.6.1) and the phase 2–4 table.

Differences from upstream (KEEP-RUST; pinned in tests and listed in docs/ui.md)

  • /api/steps is not served yet, because its builders and branch guards are phases 2–3. It answers a JSON 404 with a hint. The frontend's HTTP adapter is built without steps, so the Steps view shows upstream's own "cannot draw steps" state.
  • /api/screens answers upstream's own early return for a graph with no navigates edges. No Rust index has one until F1.
  • Branch conditions (when) are absent (phase 2).
  • Routes come from the resolvers this port has (React Router, Next.js, Vue Router, NestJS). There is no Express or Go router, so the routed fixture is NestJS.
  • There are no synthesized dispatch edges, so a flow has no dashed via … hop and Go's implicit interfaces are absent from the hierarchy.
  • An unresolved import is recorded by the binding it names, not the module specifier.
  • countImplementers and the hierarchy's overrides: false switch are MCP-only upstream and are not ported. CODEGRAPH_VIEWER_PATH has no counterpart, because the bundle is embedded.

Differences from the approved plan

  • Asset names keep Vite's content hashes. The plan said "hash-free names", but upstream serves assets/* with Cache-Control: public, max-age=31536000, immutable. With fixed names a browser would keep running the old bundle after an upgrade. Hashed names keep upstream's caching correct, and the build is still byte-reproducible: the local spike rebuilt it identically under Node 22 and Node 26, and the UI job re-checks it under Node 24.
  • The CI job is one make ui-check step, not five steps with working-directory: ui. It is the same sequence, and it is the same target make pre-ci runs locally, so local and CI cannot drift.

Verification (evidence under /tmp/evidence-ui/)

  • make pre-ci exited 0 on the pushed head a21e852 (run by the pre-push hook, pre-ci-a4-prepush.log) and on 6f6b290, the code head before the §3.6 doc commit (pre-ci-a3.log):
    • fmt, oxfmt, docs-check, actionlint, shellcheck and clippy -D warnings all pass;
    • 4342 Rust tests passed, 0 failed, 1 ignored;
    • guardrail passes;
    • make ui-check passes: svelte-check reports 0 errors, 17 files and 365 tests pass, and the bundle rebuilds byte-identically;
    • the release-archive smoke passes.
  • Golden drift check (regen-goldens.sh, release build of 6f6b290): all 19 re-indexable corpora are identical (golden-drift-a.log). This PR makes no extraction, resolution or schema change.
  • Rust tests:
    • integration suites per route against indexed fixture projects: api 58, server 26, routes 5, trails 15, events 15, map 14, flow 24, filecode 15, entrypoints 9, hierarchy 12, deadcode 19, highlight 28, codegraph_dir 1;
    • cli_ui 15, the hierarchy_bounds engine tests, and unit tests ported from upstream's own cases;
    • three observe_only watcher tests: they check the logical path, symlinked directories and ancestor removal, and that the .codegraph bytes are unchanged.
  • Browser acceptance with headless Chrome over CDP, using the release binary. Projects: a git archive of this branch (674 files, 21,906 nodes) and a NestJS routed fixture. Every route was loaded at 375 / 768 / 1280 / 1440: 40 + 16 page loads, with 0 console errors, 0 exceptions and 0 failed requests (accept-a-repo.json, accept-a-routed.json, screenshots in shots-a/).

Not in this PR

  • PR-B: direction D shell, views and states; Inter + JetBrains Mono; the Daylight theme with its System / Dark / Light switch and contrast test; responsive layouts; re-measured layout constants; theme-aware SVG export.
  • Phases 2–4 (F10/F11, F1–F6, F7–F9/F12) are scheduled in UPSTREAM.md.

🤖 Generated with Claude Code

CodeGraph Test added 6 commits October 2, 2026 20:25
The numeric spec the owner selected on 2026-10-02 (Penpot file CodeGraph Rust Design, page D · Modern Tech), verbatim (sha256 16c066581552a0f2ad6dbf7307506645a36e085cc374ced1fd4cc62f2b264d11). It is the visual contract for the browser viewer port; the board PNGs stay out of the repository.
Verbatim copy of colbymchenry/codegraph v1.6.1 (f4ddf508516332419ea3c95702810765936cf679) ui/, MIT licensed. The tree equals upstream v1.6.1:ui byte for byte (7e3b440); every change to it follows in later commits.
Pin the dependencies upstream's lock resolved, commit a standalone
package-lock.json, point the Vite build at crates/codegraph-ui/viewer, and
run upstream's frontend suites under vitest (the model suites in node, the
package suite in jsdom), plus the model cases of the three suites that mix
engine and frontend: the hierarchy layout, link placement over the tokens
the Rust highlighter sends, and the Entry points panel.

The vendored ui/ tree and its build output keep upstream formatting.
Port upstream v1.6.1's dead-code report, named-symbol flow (named and
directed modes), flow continuations, symbol lookup, type hierarchy and
syntax-token classifier, with the read-only aggregate queries they need in
codegraph-store. Add WatchOptions::observe_only, which replaces a watcher's
two sync closures with reporters so it observes changes and never writes
the index.

No extraction, resolution or schema change; golden output is unchanged.
Add the codegraph-ui crate: a loopback HTTP server that answers upstream
v1.6.1's viewer API from a project's existing index and serves the embedded
ui/ build. It keeps upstream's boundary (Host and Origin checks, write
marker, raw-path refusals, the containment chokepoint, CSP), wire shapes,
limits and error codes, saves trails under the index root, and streams
source and index changes over server-sent events from an observe-only
watcher. Screens answers upstream's no-navigation result and Steps is not
served until their builders are ported.

Add codegraph ui (alias web), gated by CODEGRAPH_UI=1 before argument
parsing exactly as upstream gates it, a UI CI job that rebuilds the bundle
and byte-checks the committed copy, and the viewer reference.
Specify the viewer's light theme as section 3.6 of the direction D spec:
the 39 Nebula token names with light values, their WCAG contrast against
the six surfaces, the effect and canvas values, and how the theme is
chosen. There are no Daylight boards; the rest of the file is unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a21e8521af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +163 to +170
let mut current = std::fs::canonicalize(start).ok()?;
loop {
if let Ok(paths) = session::resolve_paths(&current)
&& paths.current_db().is_file()
{
return Some(current);
}
if !current.pop() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the requested root before walking ancestors

When the requested path is a sensitive directory beneath an indexed ancestor, this walk promotes the request to that ancestor before cmd_ui performs validate_project_path; for example, requesting ~/.ssh can pass validation as $HOME when $HOME has an index. Validate start before ancestor lookup, or stop walking upward, so the credential-directory refusal and selected-project authority cannot be bypassed.

AGENTS.md reference: AGENTS.md:L55-L58

Useful? React with 👍 / 👎.

Comment on lines +423 to +428
FROM nodes r
JOIN edges e ON e.source = r.id
JOIN nodes h ON e.target = h.id
WHERE r.kind = 'route'
AND e.kind IN ('references', 'calls')
AND h.kind IN ('function', 'method', 'class', 'constant', 'variable')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include handlerless file-based routes in the manifest

The inner join requires every route node to have a references or calls edge, but existing file-based extractors create route nodes without such edges—for example Next.js pages in crates/codegraph-resolve/src/frameworks/react.rs:236-248 and Nuxt pages in crates/codegraph-resolve/src/frameworks/vue.rs:145-163. Consequently, even a Next/Nuxt project with many indexed routes returns routed: false; preserve route nodes with no separate handler, such as by using a left join and treating the route file/node as the navigable target.

Useful? React with 👍 / 👎.

Comment on lines +121 to +127
fn routing_manifest(ctx: &Ctx<'_>, limit: i64) -> ApiResult<Option<Manifest>> {
let rows = ctx.store.routing_manifest_rows(limit)?;
let handler_files: Vec<String> = rows.iter().map(|r| r.handler_file.clone()).collect();
let generated = ctx.store.generated_paths_among(&handler_files)?;
let filtered: Vec<RoutingRow> = rows
.into_iter()
.filter(|r| !is_low_value_file(&r.handler_file, &generated))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the route limit after low-value filtering

The SQL query is capped before test/generated handlers are removed, so when the first limit + 1 rows are low-value—such as a repository with more than 200 test routes sorted before its production routes—the later production rows are never inspected and the API can report routed: false or an incomplete, non-truncated manifest. Filter in SQL or fetch/filter until the requested number of production rows plus one has been found.

Useful? React with 👍 / 👎.

Comment on lines +123 to +127
let handler_files: Vec<String> = rows.iter().map(|r| r.handler_file.clone()).collect();
let generated = ctx.store.generated_paths_among(&handler_files)?;
let filtered: Vec<RoutingRow> = rows
.into_iter()
.filter(|r| !is_low_value_file(&r.handler_file, &generated))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter test routes by their declaration file

This checks only the handler file, so a route declared in a test or generated router file remains visible whenever it points at a production handler. That makes test-only URLs count toward the three-route threshold and appear as product entry points; classify both route_file and handler_file (and include both in the generated-path lookup) before retaining a row.

Useful? React with 👍 / 👎.

Comment on lines +547 to +550
for (id, dir) in hops {
let Some(node) = by_id.get(id) else {
missing.push(id.clone());
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve gaps when stale trail hops are missing

When a bookmarked trail contains a stale middle node ID after re-indexing, this branch silently skips it and then evaluates the next surviving node against raw.last(). The response can therefore stitch the nodes on either side of the missing hop into a new edge and return a non-partial flow labeled as the original trail; retain the gap or select only a consecutive resolved run instead of joining across missing IDs.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.60323% with 717 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-graph/src/named_symbol_flow.rs 82.76% 81 Missing ⚠️
crates/codegraph-ui/src/api/trails.rs 88.42% 77 Missing ⚠️
crates/codegraph-extract/src/syntax_tokens.rs 89.08% 43 Missing ⚠️
crates/codegraph-ui/src/api/boundary.rs 80.09% 43 Missing ⚠️
crates/codegraph-store/src/viewer.rs 91.76% 42 Missing ⚠️
crates/codegraph-graph/src/symbol_lookup.rs 82.94% 37 Missing ⚠️
crates/codegraph-graph/src/dead_code.rs 93.64% 34 Missing ⚠️
crates/codegraph-graph/src/hierarchy.rs 87.54% 34 Missing ⚠️
crates/codegraph-ui/src/api/flow.rs 94.53% 33 Missing ⚠️
crates/codegraph-ui/src/events.rs 89.89% 28 Missing ⚠️
... and 22 more

❌ Your patch check has failed because the patch coverage (91.60%) is below the target coverage (95.00%). You can increase the patch coverage or adjust the target coverage.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #298      +/-   ##
==========================================
- Coverage   95.32%   95.05%   -0.28%     
==========================================
  Files         161      197      +36     
  Lines       99751   108387    +8636     
==========================================
+ Hits        95088   103025    +7937     
- Misses       4663     5362     +699     
Files with missing lines Coverage Δ
crates/codegraph-cli/src/viewer_gate.rs 100.00% <100.00%> (ø)
crates/codegraph-graph/src/query/scoring.rs 98.60% <100.00%> (+0.03%) ⬆️
crates/codegraph-store/src/queries.rs 98.38% <100.00%> (ø)
crates/codegraph-ui/src/api/nodes.rs 100.00% <100.00%> (ø)
crates/codegraph-ui/src/api/routes.rs 100.00% <100.00%> (ø)
crates/codegraph-ui/src/api/stats.rs 100.00% <100.00%> (ø)
crates/codegraph-ui/src/assets.rs 100.00% <100.00%> (ø)
crates/codegraph-watch/src/watcher.rs 94.79% <ø> (+0.16%) ⬆️
crates/codegraph-graph/src/collation.rs 98.18% <98.18%> (ø)
crates/codegraph-ui/src/api/deadcode.rs 98.48% <98.48%> (ø)
... and 30 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

On Windows `/` resolves to the root of the current drive, which is not on
the sensitive list (upstream's own test is POSIX-only for that reason), so
the check failed on the Windows runner. Port upstream's three cases: the
POSIX roots, a normal directory, and the Windows entries in any case.
@sunerpy
sunerpy merged commit 143752a into main Oct 2, 2026
11 of 20 checks passed
@sunerpy
sunerpy deleted the feat/ui-viewer branch October 2, 2026 15:57
@github-actions github-actions Bot mentioned this pull request Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant