diff --git a/.claude/workflows/clinical-graph-rebuild.js b/.claude/workflows/clinical-graph-rebuild.js new file mode 100644 index 00000000..cc8344cf --- /dev/null +++ b/.claude/workflows/clinical-graph-rebuild.js @@ -0,0 +1,580 @@ +// Clinical-hospital graph viz rebuild — full implementation workflow. +// +// Purpose +// ------- +// Build a fresh visualization UI at ui/clinical/ that follows the +// "clinical hospital" navigation model: +// 1. Open at BIG PICTURE — domains + structural hubs only. +// 2. ZOOM IN one cut at a time (scroll/pinch) to load the next phase +// (L0 → L1 → L2 → L3 → L4 → L5 → L6). +// 3. CLICK A NODE → opens that node's chain-of-call / chain-of-action +// as a SEPARATE graph view, never polluting the main view. +// 4. Zero JS errors permitted (verify phase enforces). +// +// Foundation +// ---------- +// * Sigma.js + graphology (WebGL renderer, scales to 100 k+ nodes). +// * Vendored offline at ui/clinical/vendor/. +// * Apache Arrow JS for /api/quadtree position decode. +// +// Server contract this depends on +// ------------------------------- +// All endpoints already on origin/main (see PR #50): +// GET /api/graph/progress phase state + readiness +// GET /api/graph/phase?name=L0..L6... per-phase {nodes, edges} +// GET /api/graph cumulative JSON cache +// GET /api/graph.bin CXGB binary snapshot +// GET /api/quadtree Apache Arrow IPC (id, x, y, kind) +// GET /api/graph/events SSE batches during build +// +// Where to run +// ------------ +// From the repo root, on a machine with the dev server reachable: +// Workflow({ scriptPath: '.claude/workflows/clinical-graph-rebuild.js' }) +// +// The branch viz/ui-clinical-rebuild is pre-created and pushed; this +// workflow's Commit phase will commit + push + open a PR against main. + +export const meta = { + name: 'clinical-graph-rebuild', + description: 'Build the clinical-hospital graph viz at ui/clinical/ — Sigma.js + graphology, big-picture→zoom-in→sub-graph, zero JS errors', + phases: [ + { title: 'Spec', detail: '3 parallel design agents — navigation model, sub-graph drill-down, streaming wire-up' }, + { title: 'Scaffold', detail: 'create ui/clinical/, vendor sigma.js + graphology, write boot HTML + module skeleton' }, + { title: 'Big-pic', detail: 'implement the landing big-picture view (domains + structural backbone) from /api/graph/phase' }, + { title: 'Zoom', detail: 'progressive zoom-in: scroll/pinch deepens the visible phase (L0→L1→L2→L3)' }, + { title: 'Sub-graph', detail: 'node click opens chain-of-call/action as a SEPARATE graph view (modal/side panel)' }, + { title: 'Live', detail: 'wire /api/graph/events SSE for incremental updates after initial paint' }, + { title: 'Verify', detail: '4 parallel adversarial verifiers — JS syntax, console.error hunt, accessibility, smoke test' }, + { title: 'Commit', detail: 'final review pass + git commit + push + open PR' }, + ], +} + +const SPEC_SCHEMA = { + type: 'object', + properties: { + title: { type: 'string' }, + decisions: { + type: 'array', + items: { type: 'object', properties: { + decision: { type: 'string' }, + rationale: { type: 'string' }, + }, required: ['decision', 'rationale'] } + }, + api_endpoints_used: { type: 'array', items: { type: 'string' } }, + pseudocode: { type: 'string' }, + }, + required: ['title', 'decisions', 'api_endpoints_used', 'pseudocode'], + additionalProperties: false, +} + +const IMPL_SCHEMA = { + type: 'object', + properties: { + files_written: { type: 'array', items: { type: 'string' } }, + files_modified: { type: 'array', items: { type: 'string' } }, + summary: { type: 'string' }, + open_questions: { type: 'array', items: { type: 'string' } }, + }, + required: ['files_written', 'files_modified', 'summary'], + additionalProperties: false, +} + +const VERIFY_SCHEMA = { + type: 'object', + properties: { + check: { type: 'string' }, + passed: { type: 'boolean' }, + findings: { + type: 'array', + items: { type: 'object', properties: { + severity: { type: 'string', enum: ['blocker', 'warning', 'info'] }, + file: { type: 'string' }, + line: { type: 'number' }, + message: { type: 'string' }, + }, required: ['severity', 'message'] } + }, + }, + required: ['check', 'passed', 'findings'], + additionalProperties: false, +} + +// ── Shared context every agent gets ── +const CTX = ` +PROJECT: Cortex — neural graph visualization rebuild. +BRANCH: viz/ui-clinical-rebuild (already created, ready for commits) +TARGET: ui/clinical/ ← NEW directory, do not touch ui/unified/* or ui/dashboard/* + +DESIGN PRINCIPLE — "Clinical hospital" navigation: + 1. Open at BIG PICTURE — domains + structural hubs only (~50–500 nodes). + Like seeing the hospital from the entrance: corridors, wings, + departments. NOT every patient room at once. + 2. ZOOM IN (scroll / pinch) progresses one layer deeper. + L0 domains → L1 setup (skills/hooks/agents/commands) → L2 tools → + L3 files → L4 discussions → L5 memories → L6 symbols. + User-controlled. Each zoom level loads the next /api/graph/phase + batch and fades it in around the existing nodes. + 3. CLICK A NODE → opens that node's CHAIN-OF-CALL / CHAIN-OF-ACTION + as a SEPARATE graph view (modal, side panel, or routed page). + Does NOT pollute the main big-picture view. + 4. ZERO JavaScript errors permitted — every console.error or + uncaught throw blocks the verify phase. + +RENDERER: Sigma.js + graphology (WebGL). + Vendored offline at ui/clinical/vendor/sigma.min.js + graphology.umd.min.js + (the scaffold step downloads them). + +SERVER CONTRACT (already on the branch, no changes needed): + GET /api/graph/progress → {phase, pct, phases{key→bool}, full_ready, message, elapsed, …} + GET /api/graph/phase?name= → {nodes:[], edges:[], ready:bool, node_total:int, edge_total:int, phase:str, deps:[]} + GET /api/graph → cumulative cached snapshot (JSON) + GET /api/quadtree → Apache Arrow IPC (id, x, y, kind) — returns 503 {reason:"no_layout"} until recompute_layout has run; handle this gracefully + GET /api/graph/events → SSE batches (event:batch data:{label,nodes,edges,off,n_total,e_total}; event:done data:{total_nodes,total_edges}) + GET /api/recompute_layout → triggers DrL layout computation; call once during scaffold setup + +IMPORTANT — phase key naming: + L0..L5 are fixed keys. L6 phases are DYNAMIC: key format is "L6:" (e.g. "L6:cortex"). + NEVER hardcode "L6" as a phase key. Instead enumerate dynamic keys from /api/graph/progress .phases dict. + Example: Object.keys(progress.phases).filter(k => k.startsWith('L6:')) gives all L6 sub-phases. + +IMPORTANT — cold-start sequence: + Use SSE (/api/graph/events) as the primary data channel. Do NOT use /api/graph.bin + (CXGB binary decoder is not bundled). Cold-start = fetch L0+L1 via /api/graph/phase, + then subscribe to SSE for remaining phases. + +IMPORTANT — /api/quadtree positions: + Positions come from DrL layout (not ForceAtlas2). On fresh server start the quadtree + may return 503 {reason:"no_layout"}. Handle this: fall back to graphology's built-in + circular layout for initial render, then update positions when quadtree becomes available. + +CONSTRAINTS: + * Do not touch ui/unified/*, ui/dashboard/*, ui/methodology/* — they stay. + * No console.log spam in shipping code (warn/error/info only on real events). + * No JS syntax errors (node --check must pass on every file). + * No console.error or uncaught throw at runtime (verify phase enforces this). + * Every fetch() has a .catch() that surfaces a user-visible status, not silent fail. + * One module per concern, no monolithic file > 500 lines. + * No new MCP-server / Python changes; this rebuild is UI-only. + * Sigma v3 vendored at ui/clinical/vendor/sigma.min.js (already downloaded). + * graphology v0.25.4 vendored at ui/clinical/vendor/graphology.umd.min.js (already downloaded). + * sigma exports window.Sigma; graphology exports window.graphology — use these globals from vendor scripts. + * Duplicate node guard: track loadedPhases (Set) and pendingPhases (Set) in streaming.js; + check both before every /api/graph/phase fetch to prevent addNode on existing id (Sigma throws). + * SSE done event: call source.close() in the 'done' event handler to prevent auto-reconnect loop. +` + +phase('Spec') + +// 3 spec agents in parallel — each writes ONE markdown spec under +// ui/clinical/docs/. They don't touch JS so parallel is safe. +const specs = await parallel([ + () => agent(`${CTX} + +ROLE: Navigation-model designer for the clinical-hospital graph rebuild. + +WRITE: ui/clinical/docs/01-navigation-model.md (markdown spec, 80–150 lines). + +INCLUDE: + * The zoom-state state machine (current_depth: 0..6, transitions on + scroll, pinch, double-click). + * Which /api/graph/phase calls fire at each depth level. + * What stays visible vs what fades in vs what fades out as depth changes. + * Hit-test strategy at each depth (cursor radius, label visibility). + * Pan/zoom behaviour (free pan; zoom is the depth control). + * How a returning user lands (default depth 1? remember last?). + +RETURN the schema-shaped summary. Files written goes in 'pseudocode' +as a brief outline of the actual code structure the implementer will follow.`, + { schema: SPEC_SCHEMA, label: 'spec:navigation' }), + + () => agent(`${CTX} + +ROLE: Sub-graph drill-down designer. + +WRITE: ui/clinical/docs/02-sub-graph-drill-down.md (markdown spec, 80–150 lines). + +INCLUDE: + * How a node-click triggers a sub-graph view (modal? side panel? + routed page? Justify the choice given the navigation model.) + * Which server endpoints provide the chain-of-call / chain-of-action + data for a given node (use /api/graph for cumulative + filter + client-side, or do we need a new endpoint?). + * Sub-graph rendering: separate Sigma instance? Reuse main? Justify. + * Back-navigation from sub-graph to big-picture (and resume depth). + * Per-kind sub-graph variants (memory shows its causal chain; + symbol shows defined_in + calls + member_of; file shows tool + accesses). + +If a new server endpoint is genuinely needed, list it under +'open_questions' so the user can decide whether to add it later.`, + { schema: SPEC_SCHEMA, label: 'spec:sub-graph' }), + + () => agent(`${CTX} + +ROLE: Streaming + initial-load wire designer. + +WRITE: ui/clinical/docs/03-streaming-load.md (markdown spec, 80–150 lines). + +INCLUDE: + * Cold-start sequence: progress poll → CXGB snapshot if available → + fall back to /api/graph JSON → engage SSE for tail batches. + * How to map SSE batch events to per-depth additions without + overwhelming the renderer (queue + drain pattern; cite a max + additions per frame). + * Error/reconnect strategy when SSE drops mid-build. + * How the renderer handles a position-less node (no entry in + /api/quadtree yet) — initial spawn at neighbour centroid or domain + anchor; never NaN. + * Loading UI: progress bar wired to /api/graph/progress.pct, + phase-name display.`, + { schema: SPEC_SCHEMA, label: 'spec:streaming' }), +]) + +const validSpecs = specs.filter(Boolean) +log(`Specs complete: ${validSpecs.length}/3`) +for (const s of validSpecs) log(` - ${s.title}`) + +phase('Scaffold') + +const scaffold = await agent(`${CTX} + +ROLE: Scaffold engineer. + +CREATE the ui/clinical/ directory with this exact layout: + ui/clinical/ + index.html ← boot page with one
, + no inline scripts beyond a single + + + + + + + + diff --git a/ui/clinical/js/api.js b/ui/clinical/js/api.js new file mode 100644 index 00000000..fd7c3891 --- /dev/null +++ b/ui/clinical/js/api.js @@ -0,0 +1,114 @@ +/** + * api.js — Fetch wrappers for every server endpoint. + * + * Every function returns a Promise. + * Every error is surfaced via state.reportError(); no silent failures. + * Callers may also attach .catch() but the status-bar message is guaranteed. + */ + +import { reportError } from "./state.js"; + +const BASE = ""; // same-origin; no trailing slash + +/** + * Shared JSON fetch helper. All errors are reported to state. + * @param {string} url + * @param {string} [label] — human-readable label for error messages + * @returns {Promise<*>} + */ +async function _jsonFetch(url, label) { + const res = await fetch(BASE + url).catch(err => { + const msg = `${label || url}: network error — ${err.message}`; + reportError(msg); + throw new Error(msg); + }); + if (!res.ok) { + const msg = `${label || url}: HTTP ${res.status}`; + reportError(msg); + throw new Error(msg); + } + return res.json().catch(err => { + const msg = `${label || url}: JSON parse error — ${err.message}`; + reportError(msg); + throw new Error(msg); + }); +} + +/** + * GET /api/graph/progress + * @returns {Promise<{phase:string, pct:number, phases:Object, full_ready:boolean, message:string, elapsed:number}>} + */ +export function fetchProgress() { + return _jsonFetch("/api/graph/progress", "graph/progress"); +} + +/** + * GET /api/graph/phase?name= + * @param {string} key + * @returns {Promise<{nodes:Array, edges:Array, ready:boolean, node_total:number, edge_total:number, phase:string, deps:Array}>} + */ +export function fetchPhase(key) { + return _jsonFetch( + `/api/graph/phase?name=${encodeURIComponent(key)}`, + `graph/phase:${key}` + ); +} + +/** + * GET /api/quadtree + * Returns the Apache Arrow IPC response as an ArrayBuffer, or null on 503. + * Caller is responsible for parsing Arrow IPC. + * @returns {Promise} + */ +export async function fetchQuadtree() { + const res = await fetch(BASE + "/api/quadtree").catch(err => { + reportError(`quadtree: network error — ${err.message}`); + throw err; + }); + if (res.status === 503) { + // Not-ready is expected on cold start — not an error. + return null; + } + if (!res.ok) { + const msg = `quadtree: HTTP ${res.status}`; + reportError(msg); + throw new Error(msg); + } + return res.arrayBuffer().catch(err => { + const msg = `quadtree: buffer read error — ${err.message}`; + reportError(msg); + throw new Error(msg); + }); +} + +/** + * GET /api/recompute_layout (fire-and-forget; best-effort) + * Initiates DrL layout computation. Errors are warnings only. + * @returns {Promise} + */ +export async function triggerLayoutRecompute() { + fetch(BASE + "/api/recompute_layout").catch(err => { + console.warn("[api] recompute_layout error (non-fatal):", err.message); + }); +} + +/** + * GET /api/graph/chain?id=