mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
50d8853ea6
Interactive Viewer (primary)
────────────────────────────
codebase_graph_visualize now accepts mode="mermaid" (default, existing
behaviour — text Mermaid diagram) or mode="interactive". Interactive
mode generates a self-contained HTML page and opens it in the user's
default browser via the `open` npm package (cross-platform: macOS,
Linux, Windows). Cytoscape.js 3.30.2 + Dagre 0.8.5 + cytoscape-dagre
2.5.0 are vendored under src/assets/ — no CDN, works offline.
Features:
- File view — every source file as a node, imports as edges, language
colour-coded, circular deps highlighted in red.
- Symbol view toggle — functions/classes/methods as nodes with call
edges (confidence-styled). Embedded when the symbol graph fits under
20k symbols / 60k call edges; above that threshold the file view
remains usable and a banner directs users to codebase_impact /
codebase_symbols for symbol-level queries.
- Sidebar on node click — imports, dependents, per-file symbol list
(first 30 + link to codebase_symbols), action buttons for blast
radius and call flow.
- Right-click any node → blast radius overlay (reverse-transitive
closure). Call-flow button on the sidebar for forward traversal.
- Live search across files and symbols, six Cytoscape layouts
(Dagre / force / concentric / breadth-first / grid / circle),
PNG export (filename sanitised for cross-platform safety).
- `open: false` parameter skips auto-launch and just returns the file
path — useful in headless environments.
Viewer is XSS-safe by construction: all DOM built with createElement
+ textContent (no innerHTML anywhere); embedded JSON escapes every
"<" as \u003c so a stray </script> in a file path or symbol name
cannot break out of the script-type="application/json" container.
New files:
- src/assets/{cytoscape.min.js,dagre.min.js,cytoscape-dagre.js,
viewer-template.html,viewer-styles.css,viewer-app.js}
- scripts/copy-assets.mjs — postbuild copier (tsc does not handle
non-TS files); wired into npm run build and prepublishOnly
- src/services/graph-visualize-html.ts — HTML builder with scale-cap
logic (MAX_SYMBOLS / MAX_EDGES / MAX_SYMS_PER_FILE) and parallel
per-file Qdrant payload loading
- src/services/graph-visualize-browser.ts — temp-file write +
cross-platform open wrapper
- tests/unit/graph-visualize-html.test.ts — 5 tests (self-contained,
escape-safety, symbolMode omitted/capped, cycle marking)
- tests/unit/graph-visualize-browser.test.ts — 4 tests (deterministic
path, overwrite, success + failure paths)
New runtime dependency: open@^10.2.0 (Sindre Sorhus, zero transitive
deps, cross-platform).
British-English doc sweep (secondary)
─────────────────────────────────────
Switched all project docs to British English spelling:
behavior → behaviour organized → organised
color-coded → colour-coded initialization → initialisation
visualization → visualisation customization → customisation
recognized → recognised optimized → optimised
acknowledgment → acknowledgement finalize → finalise
analyzing → analysing apologizing → apologising
sexualized → sexualised
Affected files: README, DEVELOPER, AGENTS, CLAUDE, GEMINI, SECURITY,
CONTRIBUTING, CODE_OF_CONDUCT, agents/codebase-explorer.md,
skills/codebase-exploration/{SKILL.md,references/tool-reference.md},
skills/codebase-management/references/tool-reference.md.
Also surfaced Impact Analysis in the top-level README paragraph.
Docs
────
- README: "Interactive graph explorer" subsection under Impact Analysis,
tool-table row updated.
- DEVELOPER.md: architecture section under codebase_graph_visualize
covering asset layout, data flow, cap logic, XSS-safety invariants.
- AGENTS.md / CLAUDE.md / GEMINI.md: new "User asks for a visual /
interactive / shareable graph" row in the tool-routing table.
- skills/codebase-exploration/: SKILL.md bullet + tool-reference.md
full mode description.
- CHANGELOG.md: "Interactive Graph Explorer" section under Unreleased.
Quality gates (all green)
─────────────────────────
- Biome lint: clean
- TypeScript (tsc): clean
- Unit tests: 685/685
- Integration tests: 154/154 (real Qdrant + Ollama)
- CodeRabbit: No findings (1 fix applied — filename sanitisation)
- Snyk code test: 0 issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
|
|
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("open", () => ({ default: vi.fn().mockResolvedValue(undefined) }));
|
|
|
|
import openMock from "open";
|
|
import { openInBrowser, writeInteractiveGraphFile } from "../../src/services/graph-visualize-browser.js";
|
|
|
|
describe("writeInteractiveGraphFile", () => {
|
|
const written: string[] = [];
|
|
afterEach(async () => {
|
|
for (const f of written.splice(0)) await fs.rm(f, { force: true });
|
|
});
|
|
|
|
it("writes to a deterministic path keyed by projectId", async () => {
|
|
const html = "<!doctype html><html></html>";
|
|
const p1 = await writeInteractiveGraphFile("proj-abc", html);
|
|
written.push(p1);
|
|
expect(p1).toBe(path.join(os.tmpdir(), "socraticode-graph", "proj-abc.html"));
|
|
const read = await fs.readFile(p1, "utf-8");
|
|
expect(read).toBe(html);
|
|
});
|
|
|
|
it("overwrites on a second call for the same project", async () => {
|
|
const a = await writeInteractiveGraphFile("proj-overwrite", "<html>v1</html>");
|
|
written.push(a);
|
|
const b = await writeInteractiveGraphFile("proj-overwrite", "<html>v2</html>");
|
|
expect(a).toBe(b);
|
|
const read = await fs.readFile(b, "utf-8");
|
|
expect(read).toBe("<html>v2</html>");
|
|
});
|
|
});
|
|
|
|
describe("openInBrowser", () => {
|
|
beforeEach(() => { vi.clearAllMocks(); });
|
|
|
|
it("returns opened:true when the `open` package succeeds", async () => {
|
|
vi.mocked(openMock).mockResolvedValueOnce(undefined as unknown as never);
|
|
const result = await openInBrowser("/tmp/x.html");
|
|
expect(result.opened).toBe(true);
|
|
expect(openMock).toHaveBeenCalledWith("/tmp/x.html", { wait: false });
|
|
});
|
|
|
|
it("returns opened:false + error on failure (headless env, blocked, etc.)", async () => {
|
|
vi.mocked(openMock).mockRejectedValueOnce(new Error("xdg-open missing"));
|
|
const result = await openInBrowser("/tmp/x.html");
|
|
expect(result.opened).toBe(false);
|
|
expect(result.error).toContain("xdg-open");
|
|
});
|
|
});
|