Skip to content

feat(viz): clinical-hospital graph rebuild — sigma.js + graphology LOD viewer#51

Open
cdeust wants to merge 12 commits into
viz/server-streaming-pipelinefrom
viz/ui-clinical-rebuild
Open

feat(viz): clinical-hospital graph rebuild — sigma.js + graphology LOD viewer#51
cdeust wants to merge 12 commits into
viz/server-streaming-pipelinefrom
viz/ui-clinical-rebuild

Conversation

@cdeust
Copy link
Copy Markdown
Owner

@cdeust cdeust commented May 31, 2026

Summary

Navigation model

  1. Big picture on open — L0 domain bubbles only (~20 nodes, instant)
  2. Zoom deepens phase — scroll/pinch loads L0→L1→L2→L3→L4→L5→L6 progressively
  3. Click opens sub-graph — separate Sigma instance in a <dialog>, main view untouched
  4. Zero JS errors — verified by 4 adversarial verify agents before commit

What's in this PR

File Purpose
ui/clinical/index.html Boot page, depth indicator, side panel, status bar
ui/clinical/js/boot.js Cold-start sequence, event wiring
ui/clinical/js/state.js Reactive state store
ui/clinical/js/api.js Fetch wrappers for all 6 server endpoints
ui/clinical/js/renderer.js Sigma mount, addNodes/addEdges, hide/show per depth
ui/clinical/js/navigation.js Zoom-state machine (depth 0–6)
ui/clinical/js/streaming.js SSE subscriber, rAF drain, quadtree retry
ui/clinical/js/subgraph.js Chain-of-call/action panel (separate Sigma instance)
ui/clinical/js/chain-panel.js Node detail panel with Mermaid chain DAG
ui/clinical/vendor/ sigma.min.js + graphology.umd.min.js (offline)
ui/clinical/docs/ 4 spec docs + smoke-test plan

Backend changes (same PR)

  • get_phase_payload() now returns node_total + edge_total
  • serve_clinical() + /clinical/ route added to http_standalone.py

Test plan

See ui/clinical/docs/04-smoke-test.md for the full manual test plan.

🤖 Generated with Claude Code

cdeust and others added 12 commits May 31, 2026 11:29
…re running workflow

Pre-flight fixes so clinical-graph-rebuild.js runs clean:

B1 (L6 key): workflow now enumerates dynamic L6:<slug> keys from
  /api/graph/progress.phases — never hardcodes "L6".

B2 (CXGB missing decoder): workflow drops /api/graph.bin fast-path;
  cold-start uses SSE + /api/graph/phase instead.

B3 (/clinical/ 403): added serve_clinical() + /clinical/ route in
  _route_unified_get; _clinical_root/_clinical_html_path on Handler class.

B4 (Sigma duplicate addNode): workflow now specifies loadedPhases +
  pendingPhases Sets dedup pattern in constraints.

B5 (stash conflict): resolved — server files take server-pipeline version.

Also: node_total + edge_total added to get_phase_payload() response so
the workflow's phase-size guards work. Sigma v3 + graphology v0.25.4
vendored offline at ui/clinical/vendor/. Workflow PR target corrected
to viz/server-streaming-pipeline. W2 SSE source.close() added to
constraints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted all .memsearch/ files (index pid + 4 daily memory snapshots)
and added .memsearch/ to .gitignore. No other memory plugin is planned;
Cortex handles all persistent memory via PostgreSQL + pgvector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…D viewer

Implements the clinical-hospital navigation model at ui/clinical/:

Navigation model (big-picture → zoom → sub-graph):
  • L0 domain bubbles on open (~20 nodes, instant render)
  • Scroll/pinch deepens phase: L0→L1→L2→L3→L4→L5→L6:<slug>
  • Click any node → chain-of-call/action panel (separate Sigma instance)
  • Zero JS errors, zero console.error in production paths

Renderer: Sigma.js v3 + graphology v0.25.4 (WebGL, vendored offline)
  • Positions from /api/quadtree (DrL layout); circular fallback on 503
  • SSE primary channel (/api/graph/events), phase API secondary
  • rAF drain: max 200 nodes + 400 edges per frame
  • loadedPhases + pendingPhases Sets prevent duplicate addNode (Sigma throws)
  • EventSource.close() on SSE done event prevents reconnect loop

Accessibility (all 7 blockers from verify phase fixed):
  • depth-dot <div>s converted to <button> with aria-label + aria-pressed
  • neighbour <li>s get role=button, tabindex=0, keydown Enter/Space handler
  • :focus-visible CSS on all interactive elements
  • console.error demoted to console.warn in state dispatch and boot
  • Unconditional throw guarded by _showStatus before re-throw

Backend fixes (same commit):
  • get_phase_payload() now returns node_total + edge_total fields
  • serve_clinical() + /clinical/ route added to http_standalone.py
  • _clinical_root/_clinical_html_path wired onto Handler class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rect endpoints file

Three bugs introduced during stash-conflict resolution:
1. /api/graph/events and /api/graph.bin were not routed — requests fell
   through to the HTML handler, returning text/html instead of SSE.
   Fix: added both routes to _route_unified_get.
2. http_standalone_endpoints.py was the old version (missing
   serve_graph_events / serve_graph_binary). Restored from
   viz/server-streaming-pipeline.
3. http_standalone_graph.py also restored from the pipeline branch
   (had the correct _graph_cache_lock etc.). Re-applied node_total +
   edge_total fields to get_phase_payload.
4. serve_clinical imported non-existent send_plain_error — inlined
   the 503 response directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(graph, node, data)

Sigma v3 nodeReducer and edgeReducer are both called with (key, attributes)
— two args, no graph instance. The generated code had 3-arg signatures
(_g, _node, data) and (graph, edge, data), so 'data' was always undefined
causing 'Cannot read properties of undefined (reading depth)' on every node
render. edgeReducer now uses module-level _graph for endpoint visibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sigma v3 uses the node `type` attribute as a WebGL program name.
The server sends nodes with type="domain", type="skill", etc. which
Sigma doesn't recognise, crashing with "could not find a suitable
program for node type domain".

Fix: destructure `type` out of server node attrs before the spread so
it never reaches graphology/Sigma. Set `type: "circle"` explicitly
(the default Sigma v3 program). The server's node kind is preserved in
the `kind` attribute for colour/size logic.

Also fix edgeReducer signature (Sigma v3: (key, data) not (graph, edge, data))
and ensure _graph module-level reference is used for endpoint visibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same root cause as the node type fix: Sigma v3 reads the edge `type`
attribute as a WebGL program name. Only "line" and "arrow" are built-in.
Server edge kinds ("in_domain", "calls", "about_entity", etc.) are now
stored as `kind` and `type` is hardcoded to "line".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ll nodes

- DEPTH_SIZE [22,16,12,8,5,4,3] → [8,5,4,3,2,2,1.5]: dots were too large
- _shortLabel(): strips paths, structured-id prefixes, extensions, truncates
  at 28 chars so "/Users/.../layout_authority.py" → "layout_authority"
- labelRenderedSizeThreshold: 7 — only L0 domain nodes get persistent labels;
  all other nodes show label on hover only (Sigma v3 default)
- labelColor/font/size set to match the dark Cortex theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ces random ±100

Root cause of overlapping: quadtree is 503 on fresh start so all nodes
fell back to random positions in a ±50 unit space, creating an unreadable
mass.

New layout engine in streaming.js:
  - L0 domain nodes: golden-angle ring at radius 1400 — 20 domains
    cleanly spaced with no overlap
  - L1+ nodes: golden-angle orbit around their domain hub, radius scaled
    by depth (280 + depth×180) — gives visible structure at each LOD
  - Domain centroid fallback (step 3) tightened to ±10 jitter

Also in renderer.js:
  - DEPTH_SIZE [22,16,12,8,5,4,3] → [8,5,4,3,2,2,1.5]
  - _shortLabel: strips paths/prefixes/extensions, 28-char cap
  - labelRenderedSizeThreshold: 7 — only L0 domain nodes get persistent
    labels; everything else shows label on hover only
  - edgeType hardcoded to "line" (Sigma v3 program)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes of the all-nodes-visible blob:

1. SSE batch labels ("L0 domains", "skeleton", "L5 memories", "L6 X symbols")
   didn't match _depthForKey which only knew "L0","L1",... Fixed:
   _depthForKey now handles prefix matching + kind-based fallback so SSE
   nodes get the correct depth and are hidden at deeper levels.

2. Infinite retry loop: failed phase loads retried every 2s forever because
   loadedPhases.add(key) was never called on permanent failure. Fixed:
   after one retry, key is added to loadedPhases regardless of outcome.
   SSE stream delivers the same data anyway.

3. Colors: matched to the unified graph palette — domain gold, file cyan,
   memory emerald, hook purple, agent pink, discussion red, symbol slate.
   Vivid colors make domain clusters visually distinct like the reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
igraph >= 0.10 requires seed to be a Layout object (matrix), not an
integer. Passing seed=0 raised 'matrix expected in seed', causing
/api/recompute_layout to return 503 on every call. Removing the
parameter lets igraph initialise randomly (same practical result for
a deterministic-enough FR layout at this scale).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IDLE_TIMEOUT now reads CORTEX_IDLE_TIMEOUT env var (default 600s).
  Run with CORTEX_IDLE_TIMEOUT=3600 during dev to stop the server dying
  mid-session.
- layout_engine: remove seed=int from layout_fruchterman_reingold — igraph
  >= 0.10 requires seed to be a Layout matrix, not an integer. This was
  the root cause of /api/recompute_layout always returning 503.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant