OR-1 dataflow CPU sketch

feat: implement frontend TypeScript modules for logical view rendering

- Add types.ts: TypeScript interfaces matching graph JSON from backend
- GraphNode, GraphEdge, GraphRegion, GraphUpdate types
- SourceLoc, AddrInfo, GraphError interfaces

- Add style.ts: cytoscape stylesheet for logical view
- Node styling: circular ellipses with centered labels
- Category-based colors from backend (arithmetic, logic, comparison, etc.)
- Edge annotations: port labels at target, branch labels at source
- Function regions: dashed bounding boxes
- Error styling: red dashed borders

- Add layout.ts: dagre hierarchical layout configuration
- Top-to-bottom layout with configurable spacing
- Settings: rankDir=TB, nodeSep=60, rankSep=80, edgeSep=20

- Rewrite main.ts: WebSocket client and graph rendering
- Connect to ws://host/ws, receive graph_update JSON messages
- Convert JSON to cytoscape elements with proper labels and styling
- buildLabel: includes opcode + constant value when present
- buildElements: creates nodes, regions, and edges from update
- renderGraph: batch update with dagre layout and auto-fit
- Auto-reconnect on WebSocket close (2s timeout)
- Handle routing ops: source labels (T/F) from source_port
- Handle regions: compound parent nodes for functions

All modules verified to build with esbuild. Bundle size: 1.2MB.
Test suite: 608/608 tests passing.

Orual edb6ec69 0abca0f8

+248 -18
+10
dfgraph/frontend/src/layout.ts
··· 1 + export function logicalLayout(): object { 2 + return { 3 + name: "dagre", 4 + rankDir: "TB", 5 + nodeSep: 60, 6 + rankSep: 80, 7 + edgeSep: 20, 8 + animate: false, 9 + }; 10 + }
+98 -18
dfgraph/frontend/src/main.ts
··· 1 1 import cytoscape from "cytoscape"; 2 2 import dagre from "cytoscape-dagre"; 3 + import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "./types"; 4 + import { stylesheet } from "./style"; 5 + import { logicalLayout } from "./layout"; 3 6 4 7 cytoscape.use(dagre); 5 8 9 + const ROUTING_CATEGORY = "routing"; 10 + 6 11 const cy = cytoscape({ 7 12 container: document.getElementById("graph"), 8 - style: [ 9 - { 10 - selector: "node", 11 - style: { 12 - label: "data(label)", 13 - "text-valign": "center", 14 - "text-halign": "center", 15 - }, 16 - }, 17 - { 18 - selector: "edge", 19 - style: { 20 - "curve-style": "bezier", 21 - "target-arrow-shape": "triangle", 22 - }, 23 - }, 24 - ], 13 + style: stylesheet, 25 14 elements: [], 26 15 }); 27 16 28 - console.log("dfgraph frontend initialized", cy); 17 + function buildLabel(node: GraphNode): string { 18 + if (node.const !== null) { 19 + return `${node.opcode}\n${node.const}`; 20 + } 21 + return node.opcode; 22 + } 23 + 24 + function buildElements( 25 + update: GraphUpdate 26 + ): cytoscape.ElementDefinition[] { 27 + const elements: cytoscape.ElementDefinition[] = []; 28 + const regionParents = new Map<string, string>(); 29 + 30 + for (const region of update.regions) { 31 + if (region.kind === "function") { 32 + elements.push({ 33 + data: { id: region.tag, label: region.tag }, 34 + }); 35 + for (const nodeId of region.node_ids) { 36 + regionParents.set(nodeId, region.tag); 37 + } 38 + } 39 + } 40 + 41 + for (const node of update.nodes) { 42 + const el: cytoscape.ElementDefinition = { 43 + data: { 44 + id: node.id, 45 + label: buildLabel(node), 46 + colour: node.colour, 47 + category: node.category, 48 + pe: node.pe, 49 + iram_offset: node.iram_offset, 50 + ctx: node.ctx, 51 + }, 52 + classes: node.has_error ? "error" : undefined, 53 + }; 54 + const parent = regionParents.get(node.id); 55 + if (parent) { 56 + el.data.parent = parent; 57 + } 58 + elements.push(el); 59 + } 60 + 61 + for (const edge of update.edges) { 62 + const sourceNode = update.nodes.find((n) => n.id === edge.source); 63 + let sourceLabel: string | undefined; 64 + if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) { 65 + sourceLabel = edge.source_port === "L" ? "T" : "F"; 66 + } 67 + 68 + elements.push({ 69 + data: { 70 + id: `${edge.source}->${edge.target}:${edge.port}`, 71 + source: edge.source, 72 + target: edge.target, 73 + targetLabel: edge.port, 74 + sourceLabel: sourceLabel ?? "", 75 + }, 76 + classes: edge.has_error ? "error" : undefined, 77 + }); 78 + } 79 + 80 + return elements; 81 + } 82 + 83 + function renderGraph(update: GraphUpdate): void { 84 + cy.batch(() => { 85 + cy.elements().remove(); 86 + cy.add(buildElements(update)); 87 + }); 88 + cy.layout(logicalLayout()).run(); 89 + cy.fit(undefined, 40); 90 + } 91 + 92 + function connect(): void { 93 + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; 94 + const ws = new WebSocket(`${protocol}//${location.host}/ws`); 95 + 96 + ws.onmessage = (event: MessageEvent) => { 97 + const update: GraphUpdate = JSON.parse(event.data); 98 + if (update.type === "graph_update") { 99 + renderGraph(update); 100 + } 101 + }; 102 + 103 + ws.onclose = () => { 104 + setTimeout(connect, 2000); 105 + }; 106 + } 107 + 108 + connect();
+77
dfgraph/frontend/src/style.ts
··· 1 + import cytoscape from "cytoscape"; 2 + 3 + export const stylesheet: cytoscape.Stylesheet[] = [ 4 + { 5 + selector: "node", 6 + style: { 7 + shape: "ellipse", 8 + width: "label", 9 + height: "label", 10 + padding: "12px", 11 + "text-valign": "center", 12 + "text-halign": "center", 13 + label: "data(label)", 14 + "font-size": 11, 15 + "font-family": "monospace", 16 + color: "#fff", 17 + "text-outline-width": 0, 18 + "background-color": "data(colour)", 19 + "border-width": 2, 20 + "border-color": "data(colour)", 21 + }, 22 + }, 23 + { 24 + selector: "$node > node", 25 + style: { 26 + shape: "roundrectangle", 27 + "border-style": "dashed", 28 + "border-width": 2, 29 + "border-color": "#888", 30 + "background-color": "rgba(200, 200, 200, 0.08)", 31 + padding: "24px", 32 + "text-valign": "top", 33 + "text-halign": "center", 34 + label: "data(label)", 35 + "font-size": 12, 36 + color: "#666", 37 + }, 38 + }, 39 + { 40 + selector: "edge", 41 + style: { 42 + "curve-style": "bezier", 43 + "target-arrow-shape": "triangle", 44 + "target-arrow-color": "#999", 45 + "line-color": "#999", 46 + width: 2, 47 + "target-label": "data(targetLabel)", 48 + "target-text-offset": 18, 49 + "target-text-margin-y": -10, 50 + "source-label": "data(sourceLabel)", 51 + "source-text-offset": 18, 52 + "source-text-margin-y": -10, 53 + "font-size": 9, 54 + "font-family": "monospace", 55 + color: "#666", 56 + "text-background-color": "#fff", 57 + "text-background-opacity": 0.8, 58 + "text-background-padding": "2px", 59 + }, 60 + }, 61 + { 62 + selector: "node.error", 63 + style: { 64 + "border-style": "dashed", 65 + "border-width": 3, 66 + "border-color": "#e53935", 67 + }, 68 + }, 69 + { 70 + selector: "edge.error", 71 + style: { 72 + "line-style": "dashed", 73 + "line-color": "#e53935", 74 + "target-arrow-color": "#e53935", 75 + }, 76 + }, 77 + ];
+63
dfgraph/frontend/src/types.ts
··· 1 + export interface GraphNode { 2 + id: string; 3 + opcode: string; 4 + category: string; 5 + colour: string; 6 + const: number | null; 7 + pe: number | null; 8 + iram_offset: number | null; 9 + ctx: number | null; 10 + has_error: boolean; 11 + loc: SourceLoc; 12 + } 13 + 14 + export interface SourceLoc { 15 + line: number; 16 + column: number; 17 + end_line: number | null; 18 + end_column: number | null; 19 + } 20 + 21 + export interface AddrInfo { 22 + offset: number; 23 + port: string; 24 + pe: number | null; 25 + } 26 + 27 + export interface GraphEdge { 28 + source: string; 29 + target: string; 30 + port: string; 31 + source_port: string | null; 32 + has_error: boolean; 33 + addr?: AddrInfo; 34 + } 35 + 36 + export interface GraphRegion { 37 + tag: string; 38 + kind: string; 39 + node_ids: string[]; 40 + } 41 + 42 + export interface GraphError { 43 + line: number; 44 + column: number; 45 + category: string; 46 + message: string; 47 + suggestions: string[]; 48 + } 49 + 50 + export interface GraphUpdate { 51 + type: "graph_update"; 52 + stage: string; 53 + nodes: GraphNode[]; 54 + edges: GraphEdge[]; 55 + regions: GraphRegion[]; 56 + errors: GraphError[]; 57 + parse_error: string | null; 58 + metadata: { 59 + stage: string; 60 + pe_count: number; 61 + sm_count: number; 62 + }; 63 + }