Conversation
* Add local-VLM visual review + large-scale perf sweep to the test pipeline Two new report-only, opt-in suites that exercise realistic user experiences from multiple perspectives and at scale, plus the plumbing to run them against local GPU vision models without leaking hostnames into the repo. Large-scale perf sweep (tests/perf/scale-sweep.spec.ts, `perf-scale` project, `npm run test:perf:scale`): - Seeds real graphs (50→2000+ nodes) through the GraphQL API with grid positions, varied status/type/priority, and a connected edge backbone (canonical Edge nodes), batched. - Loads each at one or more quality tiers and records window.__graphPerf: load ms, settle ms (alpha<=0.02), avg/p95 tick, fps, dropped frames, layout drift, plus graph-scoped query p95. - generate-perf-report.mjs renders a table + inline SVG charts of how each metric scales (budgets drawn for reference). Report-only; only asserts a seeded graph renders. Cleans up each graph (edges→nodes→graph). Local VLM visual review (tests/e2e/visual-vlm.spec.ts, `vlm` project, `npm run test:vlm`): - Protocol-agnostic client (tests/helpers/vlm.ts): auto-detects OpenAI-compatible vs Ollama-native per endpoint, round-robins across all configured GPUs, bounded concurrency, lenient JSON-verdict parsing. - Judges captured states (empty graph, populated desktop+mobile, and any scale-sweep frames) from four personas: visual defects, new-user clarity, accessibility, living-graph aliveness. - generate-vlm-report.mjs renders a screenshot+verdict gallery. Report-only; asserts the model answered, not its subjective verdict. Skips entirely when VLM_ENDPOINTS is unset, so CI (which can't reach local GPUs) stays green. Hostname privacy: real endpoints live ONLY in `.env.test.local` (gitignored), auto-loaded by tests/helpers/testEnv.ts. The committed `.env.test.example` documents variable NAMES with placeholder hosts only. No hostnames/IPs/keys anywhere in the repo or docs. Wiring: dedicated Playwright projects keep these out of the fast smoke/perf gates; no CI job invokes them. Validated end-to-end locally (scale harness against the dev stack; VLM client against a mock OpenAI-compatible server). Docs: docs/testing/local-vlm-and-scale.md + SYSTEMS.md gates table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Harden VLM + scale-sweep against real local hardware Iterated after running both suites against the actual GPU boxes (gb10-02 = Qwen2.5-VL-32B on GPU, rtx4090 = Qwen2.5-VL-7B on CPU). VLM client: - Per-endpoint model resolution (VLM_MODEL=auto): boxes serving DIFFERENT models now work under one config; each verdict records which model/endpoint judged it + latency, shown in the report. - max_tokens 700->400 to keep calls fast on CPU servers. Scale sweep — make the metrics trustworthy: - Add interactionFps: real rendered frames/sec (requestAnimationFrame) measured while dragging the graph. Reliable at every size with no app instrumentation; it's the headline scaling signal (e.g. 200n=16.6fps, 500n=10.2fps observed). - Seed a FRESH graph per (size, quality): the v1 -1/settle=NONE gaps were the 2nd quality loading the 1st run's already-settled pinned positions, so the sim never ticked and window.__graphPerf never published. - Sustained node drag keeps the sim hot so __graphPerf (best-effort tick/ drift/settle bonus) publishes when it can; take the worst under-load tick. - visual-vlm: cap the slow 1920px scale-frame ingestion to the single largest; raise timeout to 900s (the all-frames version timed out at 10m). Reports: perf report leads with Interaction FPS; VLM cards show model@endpoint·latency. Docs updated (reliable vs best-effort metrics). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Large scale-sweep / VLM runs seed real graphs; an interrupted run (timeout,
Ctrl-C) skips its per-test cleanup and leaves orphan WorkItems + drifted seed
positions behind. That pollution made THE GATE's grow-flow flake (force-click
landing on an overlapping node) until a manual re-seed.
Now the heavy suites self-heal:
- All test-seeded graphs are tagged with a sentinel name prefix ("[E2E]")
so they're unmistakably identifiable (never matches a real/seed graph).
- tests/helpers/dbHealing.ts sweepTestData() (Cypher over bolt, batched,
fully graceful if Neo4j is down) removes: sentinel/legacy test-named graphs
+ their WorkItems/Edge nodes, orphan WorkItems (no BELONGS_TO), and orphan
Edge nodes (missing source/target — the data-integrity incident class).
It never touches seed/demo graphs.
- scale-sweep.spec.ts and visual-vlm.spec.ts call it in beforeAll (heal
leftovers from a prior killed run) and afterAll (clean up this run even if a
per-run delete was skipped).
Verified: injected a test graph + orphan node + orphan edge → sweep removed
exactly those, seed data (44 items) untouched; a scale run leaves 0 test
graphs / 0 orphans; THE GATE stays 5/5 green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aul) (#52) Before changing the layout system, make its problems measurable + visible. This report-only diagnostic (npm run test:geometry, 'geometry' project) seeds a controlled scenario — a CLOSE node pair and a FAR node pair sharing the same wide edge label — and measures real rendered geometry from the DOM: - edge attachment: distance from each edge endpoint to the node CENTER (0px today => edges attach to centers, not borders) and how far the endpoint sits inside the card, - label fit: label box width vs the clear span between the two cards, and whether the label box overlaps either card, - minimum length: actual edge length vs what the label needs. Baseline captured on the live stack (1440x900): close pair: centerLen=200 clearSpan=30 labelW=104 -> overflow +74px, overlapsCard=true, endpoints 0px from both centers far pair: centerLen=470 clearSpan=300 labelW=104 -> fits, no overlap Confirms: edges are center-attached, and a short edge can't fit its label so the label overflows onto the cards. Output: test-artifacts/geometry/ {report.json, scenario-full.png, close-pair-centered.png}. Seeds with the [E2E] sentinel + self-heals (sweepTestData before/after), so it leaves the dev DB clean. Re-run after a fix to verify overflow/overlap -> 0 and endpoints move to the border. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e length (#53) Two long-standing graph-layout problems, measured with the geometry diagnostic before changing anything (baseline: edges 0px from node centers; a 200px edge couldn't fit its 104px "Depends On" label -> +74px overflow onto the cards). 1. BORDER ATTACHMENT (dynamic anchor). New pure, unit-tested edgeGeometry.ts: rectBorderPoint() returns where the center line crosses a card's border; edgeBorderEndpoints() gives border-to-border endpoints. updateEdgePositions now draws the line + hitbox border-to-border and puts the arrow on the TARGET border. The anchor slides around the border as nodes move around each other -> always the shortest border-to-border connection. 2. LABEL WIDTH = MINIMUM EDGE LENGTH. minEdgeLength() = halfDiag(src) + halfDiag(tgt) + labelW + pad guarantees the label fits in the border gap at ANY angle. forceLink.distance is floored to it (rest length), and a new position-based 'minEdge' constraint force (like forceCollide, per-edge, respecting pinned nodes) makes it a HARD floor, not just a spring preference. Verified on the live stack via the diagnostic: border attachment: endpoint-to-center 0px -> 85px (on the border) for every edge. unpinned cluster (hub + 5 spokes, before the force): 1 edge overflowing, 2 labels overlapping their cards, minClearSpan 72 < labelW 106. unpinned cluster (after): 0 overflowing, 0 overlapping, minClearSpan 116 >= 107. No regressions: web units 113, perf budgets 3/3 (settle/tick/drift within budget), THE GATE 5/5, living-graph 3/3. (Pinned/user-placed nodes are deliberately left where the user put them; the floor governs the auto-layout. Drag-time clamping is a possible follow-up.) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inimum (#54) Follow-up to the border-attachment / min-edge-length work. The minEdge force governs the AUTO-layout, but a user could still drag two nodes on top of each other (the dragged node is pinned to the cursor, so the force can't move it). Now the node drag handler clamps the dragged node's target so it stays at least the edge-label minimum (edge._minLen) away from any connected neighbor that isn't moving with it (cluster-co-moving free neighbors keep their distance, so they're excluded). New pure, unit-tested helper clampToMinNeighbors() projects the target out of each neighbor's min-radius circle (a few passes resolve multiple neighbors). Verified end-to-end (geometry diagnostic): dragging one node right onto a connected anchor stops at centerDist=306 = minLen=306 — the "IS PART OF" label still displays between them. web units 118 (5 new clamp tests), THE GATE 5/5, perf budgets 3/3. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) The "Protected by ALTCHA"-style challenge centered the refresh button in the panel correctly, but in MATH mode the "What is:" label sat on top of the number and pushed the number ~14px below the panel center, so the lone refresh button (panel-centered) appeared above the number. (With the speaker button present the two-button column brackets the center, so it read as centered.) Float the "What is:" label at the top and center the NUMBER as the primary element so it sits at the panel center, aligned with the refresh button. Measured (geometry diagnostic, /signup): math refresh-vs-number offset -14px -> 0px. New report-only diagnostic tests/diagnostics/captcha-layout.spec.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uffer) (#56) minEdgeLength used the half-DIAGONAL of each card (~100px) as the node projection, leaving a far larger gap than the edge label needs. Use the per-angle border reach instead (borderReach, mirrors rectBorderPoint) so the border-to-border gap = labelWidth + a small 14px margin, at any angle. Pass the edge direction through from the link-distance accessor (the minEdge force and drag clamp reuse the cached _minLen). Also make the label-width estimate slightly generous so the gap never under-shoots the rendered label. Verified (geometry diagnostic): drag-clamp centerDist 306->274; flow cluster minClearSpan 120 >= maxLabelW 106 with 0 overflowing / 0 overlapping. web units 19 (new borderReach tests), THE GATE 5/5, perf 3/3. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d interaction audit (#57) Basic graph interactions were silently broken. Test-first, each failure shown before its fix: - Relationship TYPE change didn't update the edge label, and flipping an edge's DIRECTION left a stale duplicate. Root cause: the reinitialization gatekeeper effect keyed only on node props + edge COUNT, so an edge whose type changed (same count) or whose direction flipped (delete+create => same count, new id) never triggered a rebuild. Fix: track an edge signature (id + type + direction) and force a reinit when it changes; add the signature to the effect's dependency array so the change is observed. - The Welcome onboarding graph violated the layout rules new users are meant to learn: nodes at the unpinned origin, diagonal edges, and spacing tighter than the edge label. Re-authored the template onto an orthogonal grid (every edge horizontal or vertical, spaced for the label + small margin, every node placed/pinned, no edge routed through a non-endpoint node). Verification: - New tests/diagnostics/interaction-audit.spec.ts drives the real UI: type change -> label "Blocks" immediately (no reload); flip -> exactly one edge, direction swapped. Shown failing first, green after the fix. - New onboarding-template.test.ts asserts the Welcome template is orthogonal, spaced, connected, fully placed, and routes no edge through a node. - THE GATE 5/5, web unit 122/122, server onboarding 5/5. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t a floating overlay (#58) The HTTP warning was a fixed corner badge (`fixed top-4 right-4`) that floated over the UI and constantly overlapped page content. Replace it with a single, slim, dismissible warning strip in a standard location. - New `InsecureConnectionBanner` (replaces the floating `TlsStatusIndicator` and the unused `TlsSecurityBanner`): a full-width strip shown ONLY over HTTP, dismissible for the session. Two placements: - in-app: in-flow at the very top of the shell, so it reserves its own space and pushes the app down instead of overlapping it; - auth pages (no app chrome): a pinned top strip, portaled to <body> so a transformed ancestor can't offset it. - Layout shell is now a flex column (h-screen) with the banner as the first row and the app filling the rest; app page roots use h-full instead of h-screen so they shrink to the banner. With no banner (HTTPS) this is identical to before. Verified (over HTTP): banner at top:0, slim (33px), app starts exactly at its bottom (no overlap), no page scroll introduced, dismiss reclaims the space. New tests/diagnostics/insecure-connection-banner.spec.ts asserts all of this; THE GATE 5/5; web typecheck clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…59) First slice of Altium-style "graphs of graphs": a WorkItem can be a sheet symbol that drills into a sub-graph. - neo4j-schema.ts: WorkItem.subgraph (DRILLS_INTO -> Graph) + denormalized subgraphId scalar; inverse Graph.drillSources. Additive, no data migration. - queries.ts: fetch subgraphId + subgraph{ id name nodeCount edgeCount type } in GET_WORK_ITEMS / GET_WORK_ITEM_BY_ID. - types/graph.ts: WorkItem gains subgraphId? + subgraph? summary. Verified: GraphQL exposes the fields and returns null for existing nodes; THE GATE 5/5; web + server typecheck clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vigation (#60) * Seed guest-visible hierarchical "graphs of graphs" demo (PR2) Adds an Altium-style hierarchy demo, visible to the guest account, that showcases high-performance / dynamic LOD via drill-in (the hierarchy is the LOD strategy — never render all ~2900 nodes at once). - services/hierarchyDemo.ts + scripts/create-hierarchy-demo.ts (npm run create-hierarchy-demo): idempotent, NON-destructive. Builds a "System Overview" graph of 16 sheet-symbol WorkItems, each with subgraphId + DRILLS_INTO a sub-graph; inter-sheet wires kept endpoint-local to the overview. 16 sub-graphs (one ~1000-node "Compute Core" perf showcase), totaling 2911 work items / 4073 edges. All createdBy:'system' isShared:true (guest-visible, read-only). Edge nodes carry both EDGE_SOURCE+EDGE_TARGET. - GraphContext.tsx: fresh-load default graph now picked by identity (Welcome, then System Overview) instead of array position — shared/system demo graphs no longer hijack the default view. (The seed surfaced this latent fragility.) Verified: 17 system/shared graphs, overview sheets resolve subgraph counts, big sub-graph = 1000 nodes, idempotent re-run skips; THE GATE 5/5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drill-in/ascend navigation for hierarchical graphs (PR3) Makes the "graphs of graphs" hierarchy navigable like Altium schematics. - GraphContext: descendInto(subgraphId) / ascendTo(graphId) / getBreadcrumb() (breadcrumb derived from the graph's path ancestors + itself). Added to the context type. - InteractiveGraphVisualization: a plain click on a sheet-symbol node (one with subgraphId) descends into its sub-graph, via a ref so the D3-bound handler isn't re-created. Grow/connect, drag, and edit/relationship icons are unaffected (handled earlier / stopPropagation). - Workspace: a breadcrumb bar (Up button + clickable ancestors) shown when inside a sub-graph. Verified by tests/diagnostics/hierarchy-navigation.spec.ts: System Overview (16 sheets) -> click descends into the 1000-node Compute Core sub-graph -> breadcrumb -> Up returns to the overview. Web typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes drill-able nodes (those with a subgraphId) read as sub-sheets:
- a "stacked cards" depth effect (offset indigo rects behind the card),
- an indigo accent border (thicker),
- a bottom-right "descend" glyph (⤢) that drills in on click,
- a LOD-gated child-count line ("▸ N nodes · M edges") from the subgraph
summary fetched in PR1.
Verified: hierarchy-navigation diagnostic still green (descend into the
1000-node Compute Core sub-graph, ascend back); overview screenshot shows the
stacked/accented sheet cards + glyphs; web typecheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… camera centering, layout metrics (#62) * One-shot physics (settle then stop), spread-start layout, camera centering, layout metrics (PR-A) Graphs were "garbage piles" that drifted forever. Root causes (found via new metrics): the 2s poll re-kicked the sim every cycle (perpetual drift), and the force equilibrium itself was overlapping (an inward centering force compressed dense graphs into a core collision couldn't separate). - One-shot physics: the routine poll no longer reheats a placed/frozen graph; the sim settles to alphaMin and STAYS idle (no continuous drift). - Spread-start: unplaced nodes / Organize start on a clean grid (spacing > collision diameter) so physics REFINES a non-overlapping layout instead of failing to explode a pile from the origin. - Force tuning (physicsConfig): centering near-off (tiny containment only), stronger/longer-range charge, collision strength 1 + 4 iterations, faster cool-down + heavier damping → dense graphs settle to ZERO card overlaps and come fully to rest. Verified: a 90-node graph → 0 overlaps, sim stops ~13s. - Camera centers on the graph on load AND graph change (was keyed on hasNodes only with one stale global transform) — covers login, graph switch, drill-in. - Edge labels: a forced de-overlap pass runs at settle and for pinned graphs (which don't tick), so labels start de-overlapped. - Metrics (window.__layoutMetrics): atRest, alpha, TRUE card-overlap pairs, proximity pairs, label overlaps, settle time, drift — for studying the behaviour skeptically. window.__organizeGraph triggers a reflow. - New tests/diagnostics/physics-settle.spec.ts asserts: settles → 0 overlaps → fully stops, no drift. THE GATE 5/5 (grow flow unaffected by the tuning); web typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update physicsConfig unit test to the PR-A tuned values The defaults test pinned the old tuning (charge -60, velocityDecay 0.65, collision 0.85); PR-A intentionally retuned for a one-shot non-overlapping settle. Update the asserted values to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed (PR-B) (#63) The demo sub-graphs were laid out on a 140px grid — far under the node-card collision diameter (~224px) — so they loaded as overlapping "garbage piles". - hierarchyDemo.ts: sub-graph grid spacing 140 -> 260 (> collision diameter), so the seeded layout is non-overlapping on load with zero physics cost (matters for the 1000-node showcase). Nodes stay placed/pinned (no drift). - create-hierarchy-demo --force: deletes the existing demo (edges -> work items -> graphs, in order, so no orphan edges) and recreates it, so the spacing fix can be applied to an already-seeded DB. Verified: reseeded (17 graphs / 2911 items / 4073 edges); a 90-node sub-graph has 0 overlapping card pairs; THE GATE 5/5. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The minimap only supported click-to-navigate. Add wheel and pinch zoom so you can zoom from the minimap as well as the main view. - MiniMap.tsx: native (passive:false) wheel + 2-touch pinch listeners on the minimap svg → map the gesture point (minimap px → graph coords via the existing inverse transform) and drive the main view via window.miniMapZoom. Listener attaches when the svg actually appears (the minimap renders a "No nodes yet" div first), via a nodes-present effect dep. - InteractiveGraphVisualization.tsx: window.miniMapZoom(graphX, graphY, targetK) — zoom the main view to targetK (clamped to the [0.1,4] scaleExtent) centered on the graph point, applied through the shared zoom behavior. Verified: tests/diagnostics/minimap-zoom.spec.ts — wheel-in increases and wheel-out decreases the main view scale (0.30 → 0.49 → 0.25). Web typecheck clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The graph selector showed only top-level graphs; sub-graphs (the hierarchy) were invisible there — reachable only by drilling into the canvas. Now a parent graph expands in place to reveal its sub-graphs. - GraphSelector.tsx: a graph row with children (graph.children, from the context's existing buildHierarchy tree) gets an expand chevron; expanding it lists the sub-graphs (indented, selectable, with node/edge counts). Per-graph expand state; reuses the existing ChevronRight rotate pattern. The 'system' folder is expanded by default so the seeded "System Overview" is discoverable. Verified: tests/diagnostics/explorer-tree.spec.ts — expand "System Overview" → "Compute Core" sub-graph row appears → selecting it switches currentGraphId to subgraph-compute-shared. Web typecheck clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(PR-1) (#66) First slice of the "read contents/diagrams at a useful scale" rework. The app had NO markdown or code rendering — a node's `description` only showed as a plain textarea in a modal. NodeContentRenderer turns it into readable, GitHub-flavored markdown with syntax-highlighted fenced code, at a fixed legible size (independent of canvas zoom). - deps: react-markdown + remark-gfm + react-syntax-highlighter (Prism). - NodeContentRenderer.tsx (default export so callers can React.lazy it → stays out of the main bundle until a node's contents are first viewed). compact flag for the on-canvas peek. Styled for the dark theme; inline vs fenced code, headings, lists, tables, links. Not wired to any view yet — PR-2 (docked inspector) consumes it. Web build + typecheck clean; renderer is lazy so the heavy deps aren't in the main chunk. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (PR-2) (#67) Selecting a node opens a docked right-hand inspector with an explicit Card · Contents · Diagram toggle (per-node, session-remembered), each rendered at full legible size regardless of canvas zoom — replacing the old "zoom in to make text readable" anti-pattern. - Contents renders node.description as readable markdown + syntax-highlighted code (lazy NodeContentRenderer). - Diagram draws the node's sub-graph statically from persisted positions (NodeSubgraphPreview), capped to 300 nodes / 600 edges so even the 1000-node Compute Core sub-graph previews instantly; "Open" descends in. - Plain node click now SELECTS (opens inspector) instead of descending; sheet nodes descend via the explicit ⤢ glyph or the inspector's Open button, so a click never navigates you away unexpectedly. - handleClickOutside ignores clicks inside the inspector so its own controls don't deselect the node and close it. - Selection lifted to Workspace via onNodeSelected through SafeGraphVisualization. Verified: tests/diagnostics/node-inspector.spec.ts (Contents/Diagram/Card + legible-when-zoomed-out) and hierarchy-navigation.spec.ts green; THE GATE 5/5. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… FPS 6.7→60) (#68) Large graphs collapsed to ~7 idle FPS at HIGH quality while sitting still. Root cause (measured): every node's .node-bg carries an SVG drop-shadow halo and the living-graph animations (node-breathe / node-ache / edge-flow) run continuously, so ~1000 filtered layers repaint every frame. LOW tier already strips these and hits 60 idle FPS on the IDENTICAL DOM — proving the effects, not the node count, were the idle killer. Fix: gate those same effects off by graph SIZE, independent of the quality tier. A graph with > DENSE_GRAPH_NODE_THRESHOLD (150) nodes sets data-dense on .graph-container; new CSS rules (mirroring the LOW strips) drop the animations and the drop-shadow filter. Small graphs keep the full living-graph aesthetic at any tier — a user who picks HIGH still gets pretty small graphs and fast big ones. Measured on the Compute Core example (1000 nodes / 1400 edges), HIGH quality: idle FPS 6.7 → 59.7 (filter/blur elements 1166 → 166) Drag (1.2) and zoom (3.2) are per-tick work, addressed in following stages. Adds tests/diagnostics/large-graph-profile.spec.ts (report-only baseline: idle/drag/zoom FPS + DOM weight + data-dense confirmation). Verified: web typecheck 0 errors; living-graph e2e 3/3 (small-graph effects intact); THE GATE 5/5. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…69) When zoomed into a large graph, most nodes are off-screen yet still painted every frame — off-screen SVG costs the same to paint as on-screen. Add geometric viewport culling: node groups (and edges with both ends hidden) outside the viewport + margin get display:none, so they are neither laid out nor painted. Do-no-harm gating (learned by measurement): culling is skipped below scale 0.5 (the whole-graph "fit" view, where every node is on screen and a cull pass is pure overhead — an early attempt that culled unconditionally slowed zoom). When zoomed back out below the threshold, everything is revealed once. Culling is only enabled above 200 nodes. Recomputed on a throttle during sim ticks AND on every pan/zoom (the one-shot sim is usually stopped, so the zoom handler is the only thing that can reveal nodes panned back into view). Also throttle the minimap position-dict rebuild (every tick -> every 8th); it doesn't need 60 Hz and was rebuilding a full 1000-entry dict per tick. Measured (Compute Core, 1000n/1400e, HIGH), zoomed in to scale ~1.7 (982 nodes culled): zoomed-in drag FPS 1.2 -> 9 (~8x). Whole-graph fit-view drag/zoom unchanged (do-no-harm); idle still 60 (S1 preserved). The whole-graph view (scale ~0.1, all elements painted) is bound by element count, addressed next by simplified-node LOD. large-graph-profile.spec.ts now also measures a zoomed-in drag + culled count. Verified: web typecheck 0; THE GATE 5/5; hierarchy-navigation green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5→10) (#70) In the whole-graph "fit" view (scale ~0.1) every node's ~40 SVG sub-elements are on screen and painted each frame, and the per-tick edge pass positions 1400 arrows + labels — even though none of it is legible at that zoom. That's what makes looking at the entire graph sluggish. Add a simplified LOD: on a dense graph below SIMPLIFY_SCALE (0.45) the container gets data-simplify, and CSS hides per-node detail (title bar, type/title/desc text, status & priority bars/icons/labels, edit/relationship/descend icons) plus arrows and edge labels outright (display:none — opacity:0 still paints). Each node renders as just its colored card; edges stay so structure is legible. updateEdgePositions also skips the now-hidden arrow + label positioning per tick (via simplifiedRef), removing the bulk of the remaining per-tick cost. Zooming back in past the threshold restores full detail (and the one-shot settle pass still runs so labels are correct when shown). Measured (Compute Core, 1000n/1400e, HIGH), whole-graph fit view: zoom FPS 3.5 → 10.5 (paintedDetail 4000 → 0) drag FPS 1.2 → 4.4 idle FPS 60 → 60 (S1 preserved) Numbers are from a SERIAL profiler run (--workers=1); running the two quality tests in parallel thrashed the CPU and produced contradictory FPS — the spec is now mode:'serial'. Verified: web typecheck 0; THE GATE 5/5; node-inspector green (zoomed-in detail restores correctly). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (whole-graph pan 13→30, zoom 10→21) (#71) The earlier stages proved the whole-graph view is PAINT-bound: the browser composites every visible SVG element each time the pan/zoom transform changes, and a JS-side handler throttle did nothing (rejected). The only lever left is painting fewer elements. Below DOT_SCALE (0.2) on a dense graph — i.e. the whole-graph "fit" view — edges are sub-pixel hairlines that convey little but are ~half of what's painted after simplify. Dot mode (`data-dots`) hides all edges + their hitboxes via CSS and skips the entire 1400-edge per-tick positioning pass (dotModeRef early-return in updateEdgePositions). Each node is already just its colored card from S4, so the overview becomes a clean card grid. Edges + full detail return as you zoom in past the threshold (verified: fit → 0 edges; zoomed-in → edges + detail restored). This completes the LOD ladder the user asked for ("remove buttons and simplify nodes as we zoom out"): full detail (zoomed in) → card only, no buttons (S4, <0.45) → dots, no edges (S5, <0.2). Measured (Compute Core, 1000n/1400e, serial), whole-graph fit view: painted els ~2400 → 1000 pan FPS ~13 → 30 (HIGH) / 33 (LOW) zoom FPS 10.5 → 21 / 22 drag FPS 3 → 15 / 8 idle FPS 60 (preserved) profiler adds data-dots + paintedEls + a pan-FPS measurement. Verified: web typecheck 0; THE GATE 5/5; node-inspector + hierarchy-navigation green (zoomed-in detail/edges restore correctly). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…box, bloated inspector (#72) * Fix core interaction bugs: orphan/duplicate arrows, drag-follow edit box, bloated inspector Three dogfood bugs in the core interaction layer: 1. Flipping an edge direction added a 2nd arrow, and deleting a node left its edges' arrows behind. ROOT CAUSE: the surgical-reinit path in initializeVisualization() removed .nodes-group/.edges-group/.edge-labels-group/ .node-labels-container but NOT .arrows-group — so every rebuild appended a fresh arrows-group while the stale one (old/orphaned arrows) lingered. One line: also remove .arrows-group. Fixes both the flip-duplicate and the delete-orphan arrows (same cause). 2. The inline title-edit box didn't follow a node while dragging — its position derived from currentTransform (React state, only updated on zoom), so it lagged until release. Now an rAF loop (active only while editing) glues the overlay to the live sim position + live zoom transform, so it tracks drag, tick and zoom. 3. The right-side node inspector was a full-height 384px dock — huge and mostly empty for simple nodes. Now a compact floating card (w-72, max-h-70vh, content-height, top-right overlay) that no longer steals graph width. Also adds tests/diagnostics/core-interactions.spec.ts groundwork (a core-action matrix incl. the arrows==edges invariant that catches the arrow class of bugs). NOTE: verified by root-cause analysis; the local box is at load ~27 (shared LLM inference services), which flakes interaction tests and contaminates FPS — CI's clean runners are the authoritative check here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add core-interactions matrix scaffold (report-only; invariants gate) The 'basic user checks' checklist: each core action is an independent probe that logs PASS/FAIL so one run shows the whole picture. Structural invariants (arrows == edges before/after flip+delete, no JS errors) are asserted; the UI-trigger probes are report-only until their click/coordinate logic is hardened on a quiet machine (the dev box is at load ~27 from shared LLM services, which flakes them). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…order/icon) (#73) Changing a node's type elsewhere (dashboard/editor) updated the badge text in the graph but not the type-derived card color/border/icon — the selective update path (updateVisualizationData) refreshes text but not type visuals, and the reinit gatekeeper only watched edge signatures + node COUNT (a type change keeps count the same). Add a per-node id+type signature: when it changes, force a full reinitialization (same approach already used for edge type/flip), so the card re-renders with the new type's color, border and icon. Fixes #30. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🧪 Comprehensive Test Suite
Full-stack smoke gate runs in the CI workflow. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promote the accumulated
developwork tomainso the two branches match.Includes (this session): node Contents/Diagram inspector (#66, #67), large-graph
performance ladder — idle effect-gate, viewport culling, simplified-card LOD, dot
mode (#68, #69, #70, #71), core interaction fixes — orphan/duplicate arrows,
drag-follow edit box, compact inspector (#72), and node-type-change re-render (#73).
Closes out issues #27–#31.
All constituent PRs were CI-green on merge to develop.