Skip to content

Commit 359317e

Browse files
Merge pull request #49 from officialCodeWork/build/phase-6f/step-6f.8-visualizer
feat(cli): galaxy visualizer — self-contained interactive HTML graph explorer (6F.8)
2 parents ccfd234 + 024acc8 commit 359317e

7 files changed

Lines changed: 566 additions & 8 deletions

File tree

TRACKER.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
## Status
66

77
- **Current phase:** 6F — Field hardening, feedback round 1 (runs before 6.1–6.5)
8-
- **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)
9-
- **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
8+
- **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)
9+
- **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
1010
- **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)
1111

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

511-
### [ ] 6F.8 Galaxy visualizer (`coderadar visualize`)
511+
### [x] 6F.8 Galaxy visualizer (`coderadar visualize`)
512512
**Failure modes:** — (user-requested feature, 2026-07-15)
513513
**Build:** new CLI command `visualize -g <graph.json> -o <out.html>` emitting a single
514514
self-contained HTML file (graph JSON + all JS/CSS inlined, zero network dependencies) that
@@ -520,10 +520,27 @@ physics pause. Must stay responsive at field scale (~2.6k nodes / ~4.8k edges).
520520
**Accept:** generated from the demo-app graph: opens from `file://` with no external requests,
521521
all node kinds rendered and filterable; unit tests on generation (embedded JSON round-trips,
522522
output is self-contained); README section with a screenshot.
523-
524-
**Gate 6F:** field-patterns fixture fully green (skip list empty) · instance resolution ≥ 95% ·
525-
RTK data sources > 0 · route nodes > 0 · gibberish queries decline `no-signal` · `pnpm eval`
526-
green end-to-end.
523+
**Done:** new `visualize.ts` in the CLI package — pure `renderVisualization(graph)` (graph in,
524+
HTML string out, trivially testable) plus a `toViewModel` that trims nodes to what the client
525+
needs and drops dangling edges. The command `ui-lineage visualize -g <graph> -o <html>` writes
526+
one self-contained file: graph JSON embedded (with `<``<` so rendered text containing
527+
`</script>` can't break out), all CSS/JS inlined, no network requests. Client is a canvas
528+
force-directed sim with grid-approximated repulsion (neighbor-cell only) so it scales to
529+
thousands of nodes; node radius scales with degree; kinds color-coded with a legend.
530+
Interactions: per-kind node + edge filter toggles (with all/none), search-and-fly, click →
531+
detail panel (name, kind, detail line, file:line, connection count, flags) with neighbor
532+
highlight + dim-others (visual blast radius), drag-to-pin, scroll-zoom, physics pause,
533+
always-labels. Verified live in-browser against the demo-app graph: renders, filters, physics
534+
toggle, and search→select all work with zero console errors; `file://`/`http` both load with
535+
no external requests. CLI package gained a `test` script + vitest; 5 new tests (embed
536+
round-trips, dangling-edge drop, `</script>` breakout guard, self-contained assertions).
537+
README documents the command. eval unaffected (290/0/0/0). **Gate 6F extractor criteria met;
538+
only 6F.6 (coverage, data-blocked) remains before the gate fully closes.**
539+
540+
**Gate 6F:** field-patterns fixture fully green (skip list empty ✅) · instance resolution ≥ 95%
541+
(✅ 100%) · RTK data sources > 0 (✅) · route nodes > 0 (✅) · gibberish queries decline
542+
`no-signal` (✅) · `pnpm eval` green end-to-end (✅ 290/0/0/0). Remaining before the gate is
543+
formally stamped: 6F.6 test-coverage hardening (blocked on a real failing sample).
527544

528545
---
529546

packages/cli/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,19 @@ ui-lineage impact /api/users -g app.graph.json # blast radius: everything that
3030
ui-lineage resolve "cart total is wrong" # classify a ticket, then match it to components
3131
ui-lineage bundle "cart total is wrong" -b 4000 # a budgeted context bundle (JSON) for an agent
3232
ui-lineage correct BillingCard "amount owed" # record a correction for next time
33+
ui-lineage visualize -g app.graph.json -o app.galaxy.html # interactive HTML graph explorer
3334
```
3435

36+
`visualize` renders the whole graph as a single self-contained HTML file — the graph
37+
JSON plus all CSS/JS inlined, zero network dependencies — that opens in any browser
38+
(`open app.galaxy.html`, no server needed). Nodes are laid out as a canvas
39+
force-directed "galaxy", color-coded by kind (component, instance, hook, data-source,
40+
state, event, route, external, test); components are the stars, their data/state/events
41+
cluster around them, routes act as gravitational centers. Toggle node and edge kinds,
42+
search a node and fly to it, click a node for its details plus a highlighted
43+
neighborhood (a visual blast radius), drag to pin, scroll to zoom, and pause the
44+
physics. Built to stay responsive at real-codebase scale (thousands of nodes).
45+
3546
`impact <node>` takes a component name, an API endpoint, a state name, or a route
3647
path, and lists every node that depends on it (reverse traversal), each indented by
3748
its distance — so a change can be reviewed for what it might break:

packages/cli/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"scripts": {
4444
"build": "tsup",
4545
"typecheck": "tsc -p tsconfig.json --noEmit",
46+
"test": "vitest run",
4647
"prepublishOnly": "pnpm build"
4748
},
4849
"dependencies": {
@@ -60,6 +61,7 @@
6061
"@coderadar/vision": "workspace:*",
6162
"@types/node": "^22.20.1",
6263
"tsup": "^8.5.1",
63-
"typescript": "^5.7.0"
64+
"typescript": "^5.7.0",
65+
"vitest": "^3.2.7"
6466
}
6567
}

packages/cli/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { resolveHookEdges, scanReact } from "@coderadar/parser-react";
2323
import { Command } from "commander";
2424
import { parse as parseYaml } from "yaml";
2525

26+
import { renderVisualization } from "./visualize.js";
27+
2628
const program = new Command();
2729

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

277+
program
278+
.command("visualize")
279+
.description("Render the lineage graph as a self-contained interactive HTML galaxy")
280+
.option("-g, --graph <file>", "graph file", "ui-lineage.graph.json")
281+
.option("-o, --out <file>", "output HTML file", "ui-lineage.galaxy.html")
282+
.option("-t, --title <title>", "page title")
283+
.action((opts: { graph: string; out: string; title?: string }) => {
284+
const graph = loadGraph(opts.graph);
285+
const html = renderVisualization(graph, opts.title);
286+
fs.writeFileSync(opts.out, html);
287+
const kb = (Buffer.byteLength(html) / 1024).toFixed(0);
288+
console.log(
289+
`Galaxy written to ${opts.out} (${kb} KB, ${graph.nodes.length} nodes / ${graph.edges.length} edges).`,
290+
);
291+
console.log(`Open it in a browser: open ${opts.out}`);
292+
});
293+
275294
const STEP_ARROW: Record<string, string> = {
276295
page: "▸",
277296
event: "•",

packages/cli/src/visualize.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { LineageGraph } from "@coderadar/core";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { renderVisualization, toViewModel } from "./visualize.js";
5+
6+
const graph: LineageGraph = {
7+
version: 2,
8+
root: "/app",
9+
generatedAt: "2026-07-15T00:00:00.000Z",
10+
generator: "test",
11+
nodes: [
12+
{
13+
id: "component:App.tsx#App",
14+
kind: "component",
15+
name: "App",
16+
loc: { file: "App.tsx", line: 1, endLine: 9 },
17+
exportName: "App",
18+
// A prop carrying a </script> payload — it flows into the node detail
19+
// and thus the embedded JSON, so it exercises the breakout guard.
20+
props: ["title", "</script><b>x"],
21+
renderedText: [{ text: "Hello world", source: "jsx" }],
22+
rendersComponents: [],
23+
structure: {
24+
table: 0, columns: 0, form: 0, input: 0, button: 0,
25+
link: 0, image: 0, heading: 1, list: 0, repeated: 0,
26+
},
27+
},
28+
{
29+
id: "data-source:api.ts#rtk-query:/api/users",
30+
kind: "data-source",
31+
name: "/api/users",
32+
loc: { file: "api.ts", line: 4, endLine: 4 },
33+
sourceKind: "rtk-query",
34+
method: "GET",
35+
endpoint: "/api/users",
36+
raw: '"/users"',
37+
resolved: "full",
38+
},
39+
],
40+
edges: [
41+
{ from: "component:App.tsx#App", to: "data-source:api.ts#rtk-query:/api/users", kind: "fetches-from" },
42+
// Dangling edge — must be dropped, not rendered.
43+
{ from: "component:App.tsx#App", to: "component:Ghost.tsx#Ghost", kind: "renders" },
44+
],
45+
};
46+
47+
describe("visualize view model (TRACKER 6F.8)", () => {
48+
it("trims nodes and drops edges to unknown endpoints", () => {
49+
const model = toViewModel(graph);
50+
expect(model.nodes).toHaveLength(2);
51+
expect(model.edges).toHaveLength(1);
52+
expect(model.edges[0]?.kind).toBe("fetches-from");
53+
});
54+
55+
it("summarizes each node kind for the detail panel", () => {
56+
const model = toViewModel(graph);
57+
const ds = model.nodes.find((n) => n.kind === "data-source");
58+
expect(ds?.detail).toBe("GET /api/users (rtk-query)");
59+
expect(ds?.label).toBe("/api/users");
60+
});
61+
});
62+
63+
describe("visualize HTML output", () => {
64+
const html = renderVisualization(graph, "My Galaxy");
65+
66+
it("is a self-contained document with no external requests", () => {
67+
expect(html.startsWith("<!doctype html>")).toBe(true);
68+
expect(html).toContain("<title>My Galaxy</title>");
69+
// No network dependencies: no external src/href/fetch of remote origins.
70+
expect(html).not.toMatch(/src="https?:/);
71+
expect(html).not.toMatch(/href="https?:/);
72+
expect(html).not.toMatch(/@import/);
73+
});
74+
75+
it("embeds the graph JSON safely against </script> breakout", () => {
76+
// A prop carries "</script>" — the embed must escape it so it can't close
77+
// the data <script> early. The literal must not appear; the escaped form must.
78+
expect(html).toContain("\\u003c/script>\\u003cb>x");
79+
// Exactly the data script + the app script close normally — no injected close.
80+
expect(html.match(/<\/script>/g)?.length).toBe(2);
81+
});
82+
83+
it("inlines every node kind's color so the legend renders offline", () => {
84+
expect(html).toContain("#4f9dff"); // component
85+
expect(html).toContain("#ff8f6b"); // data-source
86+
});
87+
});

0 commit comments

Comments
 (0)