From 24a602a407814b50935933ba988fbe4f03117849 Mon Sep 17 00:00:00 2001 From: daksh Date: Tue, 23 Jun 2026 23:57:00 +0530 Subject: [PATCH] feat: add visualization graph --- README.md | 1 + apps/cli/main.ts | 1 + libs/knowledge-graph/folder-export.ts | 5 + libs/knowledge-graph/html-export.ts | 2017 +++++++++++++++++++++++++ 4 files changed, 2024 insertions(+) create mode 100644 libs/knowledge-graph/html-export.ts diff --git a/README.md b/README.md index a483ed3..f4fe452 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ greplica proposal apply - `greplica graph context ""` - returns Markdown for agent use. Add `--debug` for the full retrieval payload with ranking signals. - `greplica graph read` - prints the current graph view: all components, flows, claims, sources, and edges in scope. - `greplica graph view` to visualise the current memory in a local HTML, opens in your default browser. Use `--out` to choose where the file is written; by default it goes to a temp path. +- `greplica graph export ` - writes an offline graph export. Open `/index.html` for a self-contained visualization with search, node-type filters, node details, and immediate relationships. The export also includes Markdown index files for browsing components, flows, claims, and sources. - `greplica transcript bundle` - converts one or more Codex, Claude Code, or GitHub Copilot CLI JSONL transcripts into a sanitized Markdown bundle for `greplica-fast-session-bootstrap`. - `greplica doctor` - verifies installation and diagnoses configuration failures. Not a required preflight before every command. - `greplica install` prepares repo state, local storage, and agent integration; normal repo commands require install first. diff --git a/apps/cli/main.ts b/apps/cli/main.ts index d6c0f42..0b4a993 100644 --- a/apps/cli/main.ts +++ b/apps/cli/main.ts @@ -285,6 +285,7 @@ function runGraphExportCommand(args: string[]): void { const files = buildGraphFolderExport(service.readGraph(repo)); writeGraphFolderExport(outputDir, files); console.log(`Exported current graph view to ${outputDir}`); + console.log(`HTML: ${join(outputDir, "index.html")}`); console.log(`Files: ${files.length}`); } diff --git a/libs/knowledge-graph/folder-export.ts b/libs/knowledge-graph/folder-export.ts index e660ab7..66e51b0 100644 --- a/libs/knowledge-graph/folder-export.ts +++ b/libs/knowledge-graph/folder-export.ts @@ -4,6 +4,7 @@ import type { Claim, ClaimKind } from "./claim.js"; import type { Edge } from "./edge.js"; import type { GraphReadResult } from "./service.js"; import type { Component, Flow, Source } from "./schema.js"; +import { buildGraphHtmlExport } from "./html-export.js"; export interface ExportedGraphFile { path: string; @@ -35,6 +36,10 @@ export function buildGraphFolderExport(graph: GraphReadResult): ExportedGraphFil path: "index.md", content: renderRootIndex(graph, components, flows), }, + { + path: "index.html", + content: buildGraphHtmlExport(graph), + }, { path: "sources.md", content: renderSources(graph.sources, graph.claims, evidenceByClaimId), diff --git a/libs/knowledge-graph/html-export.ts b/libs/knowledge-graph/html-export.ts new file mode 100644 index 0000000..6821833 --- /dev/null +++ b/libs/knowledge-graph/html-export.ts @@ -0,0 +1,2017 @@ +import type { Claim } from "./claim.js"; +import type { Edge } from "./edge.js"; +import type { GraphReadResult } from "./service.js"; +import type { Component, Flow, GraphObjectType, Source } from "./schema.js"; + +type GraphNodeType = "component" | "flow" | "claim" | "source"; + +interface ExportNode { + id: string; + type: GraphNodeType; + label: string; + detail: Record; + search: string; +} + +interface ExportEdge { + id: string; + kind: Edge["kind"]; + from: string; + fromType: GraphObjectType; + to: string; + toType: GraphObjectType; + metadata?: Edge["metadata"]; +} + +interface HtmlGraphData { + generatedAt: string; + counts: { + components: number; + flows: number; + claims: number; + sources: number; + edges: number; + }; + nodes: ExportNode[]; + edges: ExportEdge[]; +} + +export function buildGraphHtmlExport(graph: GraphReadResult): string { + return renderHtml(graphToHtmlData(graph)); +} + +function graphToHtmlData(graph: GraphReadResult): HtmlGraphData { + const nodes: ExportNode[] = [ + ...graph.components.map(componentNode), + ...graph.flows.map(flowNode), + ...graph.claims.map(claimNode), + ...graph.sources.map(sourceNode), + ]; + + return { + generatedAt: new Date().toISOString(), + counts: { + components: graph.components.length, + flows: graph.flows.length, + claims: graph.claims.length, + sources: graph.sources.length, + edges: graph.edges.length, + }, + nodes, + edges: graph.edges.map((edge) => ({ + id: edge.id, + kind: edge.kind, + from: edge.from_id, + fromType: edge.from_type, + to: edge.to_id, + toType: edge.to_type, + metadata: edge.metadata, + })), + }; +} + +function componentNode(component: Component): ExportNode { + return node("component", component.id, component.name, { + ID: component.id, + Type: "component", + Name: component.name, + "Code anchor": component.code_anchor, + }); +} + +function flowNode(flow: Flow): ExportNode { + return node("flow", flow.id, flow.name, { + ID: flow.id, + Type: "flow", + Name: flow.name, + }); +} + +function claimNode(claim: Claim): ExportNode { + return node("claim", claim.id, claim.text, { + ID: claim.id, + Type: "claim", + Kind: claim.kind, + Truth: claim.truth, + Intent: claim.intent, + Text: claim.text, + }); +} + +function sourceNode(source: Source): ExportNode { + const label = source.title ?? source.ref; + return node("source", source.id, label, { + ID: source.id, + Type: "source", + Kind: source.kind, + Ref: source.ref, + Title: source.title, + }); +} + +function node(type: GraphNodeType, id: string, label: string, detail: Record): ExportNode { + const filteredDetail = Object.fromEntries( + Object.entries(detail).filter((entry): entry is [string, string] => entry[1] !== undefined && entry[1].length > 0), + ); + const search = [id, label, ...Object.values(filteredDetail)].join(" ").toLowerCase(); + return { id, type, label, detail: filteredDetail, search }; +} + +function renderHtml(data: HtmlGraphData): string { + return ` + + + + +Greplica Graph Export + + + + + + +
+
+
+ Greplica +
+
+ +
0 nodes · 0 edges
+
+ +
+ + 100% + +
+ + +
+ + + + +
+ +
+
+ + + +
+ + + + + +`; +} + +function escapeJsonForHtml(data: HtmlGraphData): string { + return JSON.stringify(data) + .replace(//g, "\\u003e") + .replace(/&/g, "\\u0026") + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029"); +}