Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
## Status

- **Current phase:** 6F — Field hardening, feedback round 1 (runs before 6.1–6.5)
- **Next step:** 6F.8 galaxy visualizer · 6F.6 test-coverage hardening (blocked: needs the field's actual failing variant — fixture's wrapper shape already passes)
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5, 6F.7
- **Next step:** 6F.6 test-coverage hardening (blocked: needs the field's actual failing variant — the fixture's custom-wrapper shape already passes; get a failing test file from the tester or ship the defensive `coverage-unmapped` half only)
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5, 6F.7–6F.8
- **Gates passed:** Gate 0 (CI + red-path, #5/#6) · Gate 1 (precision 1.000, recall 0.895, zero poison) · Gate 2 (C1 instance attribution 1.000 · B1 4-level handler chains · C6 store writers↔readers · A9 portals — scorecard 137/0/0, precision & recall 1.000) · Gate 3 (B3 action effects · B4 routers · B6 cyclic journeys terminate · B7/B8 form & non-JSX events · G5 flag/role conditions — precision & recall 1.000) · Gate 4 (A4 rarity · A10 fuzzy/OCR · A1 structural · A6 subtree · E3 vision annotations · E2 aliases · G4 corrections — high-conf correct 1.000, ambiguity honesty 1.000, poison rate 0.000) · Gate 5 (F1 context bundle · F2 blast radius · F3 test coverage · F4 response schema · F5 git history · MCP server over stdio — scorecard 265/0/0, all honesty metrics 1.000; **M5 reached** — ticket in → budgeted context bundle out, over MCP)

## What CodeRadar is
Expand Down Expand Up @@ -508,7 +508,7 @@ printed by the CLI (`score=… confidence=…`); MCP inherits it through the env
Candidate isn't part of the generated schemas, so no schema change (drift gate confirms).
2 new core tests (211 total); eval 290/0/0/0, gate OK, metrics 1.000.

### [ ] 6F.8 Galaxy visualizer (`coderadar visualize`)
### [x] 6F.8 Galaxy visualizer (`coderadar visualize`)
**Failure modes:** — (user-requested feature, 2026-07-15)
**Build:** new CLI command `visualize -g <graph.json> -o <out.html>` emitting a single
self-contained HTML file (graph JSON + all JS/CSS inlined, zero network dependencies) that
Expand All @@ -520,10 +520,27 @@ physics pause. Must stay responsive at field scale (~2.6k nodes / ~4.8k edges).
**Accept:** generated from the demo-app graph: opens from `file://` with no external requests,
all node kinds rendered and filterable; unit tests on generation (embedded JSON round-trips,
output is self-contained); README section with a screenshot.

**Gate 6F:** field-patterns fixture fully green (skip list empty) · instance resolution ≥ 95% ·
RTK data sources > 0 · route nodes > 0 · gibberish queries decline `no-signal` · `pnpm eval`
green end-to-end.
**Done:** new `visualize.ts` in the CLI package — pure `renderVisualization(graph)` (graph in,
HTML string out, trivially testable) plus a `toViewModel` that trims nodes to what the client
needs and drops dangling edges. The command `ui-lineage visualize -g <graph> -o <html>` writes
one self-contained file: graph JSON embedded (with `<` → `<` so rendered text containing
`</script>` can't break out), all CSS/JS inlined, no network requests. Client is a canvas
force-directed sim with grid-approximated repulsion (neighbor-cell only) so it scales to
thousands of nodes; node radius scales with degree; kinds color-coded with a legend.
Interactions: per-kind node + edge filter toggles (with all/none), search-and-fly, click →
detail panel (name, kind, detail line, file:line, connection count, flags) with neighbor
highlight + dim-others (visual blast radius), drag-to-pin, scroll-zoom, physics pause,
always-labels. Verified live in-browser against the demo-app graph: renders, filters, physics
toggle, and search→select all work with zero console errors; `file://`/`http` both load with
no external requests. CLI package gained a `test` script + vitest; 5 new tests (embed
round-trips, dangling-edge drop, `</script>` breakout guard, self-contained assertions).
README documents the command. eval unaffected (290/0/0/0). **Gate 6F extractor criteria met;
only 6F.6 (coverage, data-blocked) remains before the gate fully closes.**

**Gate 6F:** field-patterns fixture fully green (skip list empty ✅) · instance resolution ≥ 95%
(✅ 100%) · RTK data sources > 0 (✅) · route nodes > 0 (✅) · gibberish queries decline
`no-signal` (✅) · `pnpm eval` green end-to-end (✅ 290/0/0/0). Remaining before the gate is
formally stamped: 6F.6 test-coverage hardening (blocked on a real failing sample).

---

Expand Down
11 changes: 11 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ ui-lineage impact /api/users -g app.graph.json # blast radius: everything that
ui-lineage resolve "cart total is wrong" # classify a ticket, then match it to components
ui-lineage bundle "cart total is wrong" -b 4000 # a budgeted context bundle (JSON) for an agent
ui-lineage correct BillingCard "amount owed" # record a correction for next time
ui-lineage visualize -g app.graph.json -o app.galaxy.html # interactive HTML graph explorer
```

`visualize` renders the whole graph as a single self-contained HTML file — the graph
JSON plus all CSS/JS inlined, zero network dependencies — that opens in any browser
(`open app.galaxy.html`, no server needed). Nodes are laid out as a canvas
force-directed "galaxy", color-coded by kind (component, instance, hook, data-source,
state, event, route, external, test); components are the stars, their data/state/events
cluster around them, routes act as gravitational centers. Toggle node and edge kinds,
search a node and fly to it, click a node for its details plus a highlighted
neighborhood (a visual blast radius), drag to pin, scroll to zoom, and pause the
physics. Built to stay responsive at real-codebase scale (thousands of nodes).

`impact <node>` takes a component name, an API endpoint, a state name, or a route
path, and lists every node that depends on it (reverse traversal), each indented by
its distance — so a change can be reviewed for what it might break:
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"scripts": {
"build": "tsup",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"prepublishOnly": "pnpm build"
},
"dependencies": {
Expand All @@ -60,6 +61,7 @@
"@coderadar/vision": "workspace:*",
"@types/node": "^22.20.1",
"tsup": "^8.5.1",
"typescript": "^5.7.0"
"typescript": "^5.7.0",
"vitest": "^3.2.7"
}
}
19 changes: 19 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { resolveHookEdges, scanReact } from "@coderadar/parser-react";
import { Command } from "commander";
import { parse as parseYaml } from "yaml";

import { renderVisualization } from "./visualize.js";

const program = new Command();

program
Expand Down Expand Up @@ -272,6 +274,23 @@ program
console.log(`Recorded: [${terms.join(", ")}] → ${component} (${opts.corrections})`);
});

program
.command("visualize")
.description("Render the lineage graph as a self-contained interactive HTML galaxy")
.option("-g, --graph <file>", "graph file", "ui-lineage.graph.json")
.option("-o, --out <file>", "output HTML file", "ui-lineage.galaxy.html")
.option("-t, --title <title>", "page title")
.action((opts: { graph: string; out: string; title?: string }) => {
const graph = loadGraph(opts.graph);
const html = renderVisualization(graph, opts.title);
fs.writeFileSync(opts.out, html);
const kb = (Buffer.byteLength(html) / 1024).toFixed(0);
console.log(
`Galaxy written to ${opts.out} (${kb} KB, ${graph.nodes.length} nodes / ${graph.edges.length} edges).`,
);
console.log(`Open it in a browser: open ${opts.out}`);
});

const STEP_ARROW: Record<string, string> = {
page: "▸",
event: "•",
Expand Down
87 changes: 87 additions & 0 deletions packages/cli/src/visualize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { LineageGraph } from "@coderadar/core";
import { describe, expect, it } from "vitest";

import { renderVisualization, toViewModel } from "./visualize.js";

const graph: LineageGraph = {
version: 2,
root: "/app",
generatedAt: "2026-07-15T00:00:00.000Z",
generator: "test",
nodes: [
{
id: "component:App.tsx#App",
kind: "component",
name: "App",
loc: { file: "App.tsx", line: 1, endLine: 9 },
exportName: "App",
// A prop carrying a </script> payload — it flows into the node detail
// and thus the embedded JSON, so it exercises the breakout guard.
props: ["title", "</script><b>x"],
renderedText: [{ text: "Hello world", source: "jsx" }],
rendersComponents: [],
structure: {
table: 0, columns: 0, form: 0, input: 0, button: 0,
link: 0, image: 0, heading: 1, list: 0, repeated: 0,
},
},
{
id: "data-source:api.ts#rtk-query:/api/users",
kind: "data-source",
name: "/api/users",
loc: { file: "api.ts", line: 4, endLine: 4 },
sourceKind: "rtk-query",
method: "GET",
endpoint: "/api/users",
raw: '"/users"',
resolved: "full",
},
],
edges: [
{ from: "component:App.tsx#App", to: "data-source:api.ts#rtk-query:/api/users", kind: "fetches-from" },
// Dangling edge — must be dropped, not rendered.
{ from: "component:App.tsx#App", to: "component:Ghost.tsx#Ghost", kind: "renders" },
],
};

describe("visualize view model (TRACKER 6F.8)", () => {
it("trims nodes and drops edges to unknown endpoints", () => {
const model = toViewModel(graph);
expect(model.nodes).toHaveLength(2);
expect(model.edges).toHaveLength(1);
expect(model.edges[0]?.kind).toBe("fetches-from");
});

it("summarizes each node kind for the detail panel", () => {
const model = toViewModel(graph);
const ds = model.nodes.find((n) => n.kind === "data-source");
expect(ds?.detail).toBe("GET /api/users (rtk-query)");
expect(ds?.label).toBe("/api/users");
});
});

describe("visualize HTML output", () => {
const html = renderVisualization(graph, "My Galaxy");

it("is a self-contained document with no external requests", () => {
expect(html.startsWith("<!doctype html>")).toBe(true);
expect(html).toContain("<title>My Galaxy</title>");
// No network dependencies: no external src/href/fetch of remote origins.
expect(html).not.toMatch(/src="https?:/);
expect(html).not.toMatch(/href="https?:/);
expect(html).not.toMatch(/@import/);
});

it("embeds the graph JSON safely against </script> breakout", () => {
// A prop carries "</script>" — the embed must escape it so it can't close
// the data <script> early. The literal must not appear; the escaped form must.
expect(html).toContain("\\u003c/script>\\u003cb>x");
// Exactly the data script + the app script close normally — no injected close.
expect(html.match(/<\/script>/g)?.length).toBe(2);
});

it("inlines every node kind's color so the legend renders offline", () => {
expect(html).toContain("#4f9dff"); // component
expect(html).toContain("#ff8f6b"); // data-source
});
});
Loading
Loading