From 43ada01ffeb213006289db5e20d3e36cebf2464c Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 19:09:01 -0700 Subject: [PATCH 01/13] ADAPT-7: add nested semantic cuts and parent navigation --- .github/workflows/ci.yml | 5 + docs/USER_STORIES.md | 2 +- docs/architecture/dynamic-graph-lod.md | 43 +- .../NESTED_LOD_VALIDATION_2026-10-28.md | 66 ++++ .../src/components/SemanticGraphOverview.tsx | 200 ++++++++-- packages/web/src/lib/semanticLOD.test.ts | 275 +++++++++++++ packages/web/src/lib/semanticLOD.ts | 325 ++++++++++++---- tests/e2e/graph/nested-semantic-lod.spec.ts | 366 ++++++++++++++++++ tests/lib/runner/unified-cli.test.mjs | 6 +- tests/sequences/unified.config.mjs | 7 +- 10 files changed, 1190 insertions(+), 105 deletions(-) create mode 100644 docs/quality/NESTED_LOD_VALIDATION_2026-10-28.md create mode 100644 tests/e2e/graph/nested-semantic-lod.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fafbb7e..8459f07d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,6 +194,11 @@ jobs: env: TEST_URL: http://localhost:3127 CI: true + - name: Nested semantic LOD acceptance + run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence nested-lod --out test-artifacts/ci-nested-lod + env: + TEST_URL: http://localhost:3127 + CI: true - name: Durable layout recovery acceptance run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence layout-recovery --out test-artifacts/ci-layout-recovery env: diff --git a/docs/USER_STORIES.md b/docs/USER_STORIES.md index 83dc23dc..1a39cf81 100644 --- a/docs/USER_STORIES.md +++ b/docs/USER_STORIES.md @@ -35,7 +35,7 @@ Why this exists: everybody hates Jira. GraphDone wins by being **alive, fast eve | ADAPT-4 | As a user with a huge graph on a slow link, I want the graph streamed by relevance (my items + center-of-gravity first, periphery progressively), so first paint is fast. | Initial query bounded (≤ 150 nodes); progressive fetch fills periphery in background; loading affordance on unexpanded frontier; TTFP < 2s on simulated 3G for 1k-node graph. | server test `server/src/__tests__/progressive-loading.test.ts`, e2e `tests/e2e/adaptive.spec.ts` | 💤 | | ADAPT-5 | As a user who upgrades conditions (wifi, plugged in), I want quality to step back up automatically, so I get the pretty version when I can afford it. | `connection.change` listener re-evaluates tier; hysteresis prevents flapping (≥ 10s between step-ups); unit-tested state machine. | `adaptiveQuality.test.ts` | ✅ | | ADAPT-6 | As a user, I want to optionally pin a quality tier in Settings (Auto / Low / High...), so I stay in control when auto guesses wrong. | Settings override persists (localStorage); "Auto" returns to detection; UI shows current effective tier. | `adaptiveQuality.test.ts`, `adaptive.spec.ts` | ✅ | -| ADAPT-7 | As a phone user, I want LOD (level-of-detail) tuned per tier — labels, icons, minimap appear/disappear by zoom *and* tier — so small screens stay readable and fast. | LOD policy uses tier, projected size and density; individual detail is replaced by readable group names/counts at far zoom, including LOW. | `adaptiveQuality.test.ts`; semantic browser coverage planned in the [LOD continuation](architecture/dynamic-graph-lod.md) | 💤 | +| ADAPT-7 | As a phone user, I want LOD (level-of-detail) tuned per tier — labels, icons, minimap appear/disappear by zoom *and* tier — so small screens stay readable and fast. | LOD policy uses tier, projected size and density; individual detail is replaced by readable group names/counts at far zoom, including LOW. | `adaptiveQuality.test.ts`; `semantic-lod.spec.ts` and `nested-semantic-lod.spec.ts` cover readable overview, nested cuts, parent fallback and navigation. Per-region/tier budgets remain in the [LOD continuation](architecture/dynamic-graph-lod.md). | 🔨 | | ADAPT-8 | As a developer, I want performance budgets enforced in CI (bundle size, TTI, fps on reference graph), so regressions get caught before merge. | Bundle gate and strict measured four-node fixture are implemented. The earlier 500-node first-render/pan targets are not established by that fixture; larger graph enforcement remains pending in the [LOD continuation](architecture/dynamic-graph-lod.md). | `tests/perf/check-bundle-size.mjs` + `tests/perf/graph-perf.spec.ts`; `tests/perf/scale-sweep.spec.ts` remains report-only | 🧪 | | ADAPT-9 | As a user with `prefers-reduced-motion`, I want all animation suppressed regardless of tier, so accessibility beats aesthetics. | Reduced-motion forces animation-free rendering at any tier; verified in unit + e2e. | `adaptiveQuality.test.ts`, `living-graph.spec.ts` | ✅ | diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md index 0bb4fa97..161118f2 100644 --- a/docs/architecture/dynamic-graph-lod.md +++ b/docs/architecture/dynamic-graph-lod.md @@ -1,8 +1,10 @@ # Dynamic graph LOD: continuation through delivery -Reviewed 2026-10-27 UTC. Status: the first production-renderer overview slice is -implemented on the feature branch (section 8); the full dynamic LOD path remains -open. Source, GitHub and localhost observations below describe the reviewed base. +Baseline reviewed 2026-10-27 UTC; implementation status updated 2026-10-28 UTC. +The first production-renderer overview and durable layout recovery are integrated +into dev. The nested hierarchy continuation is under validation in #215 (section 9). +The full dynamic LOD path remains open. Source, GitHub and localhost observations +below describe the original reviewed base, not current branch tips. The stage IDs below are proposed work items, not existing GitHub issue numbers. The owner's CodeGraph reuse direction is incorporated in the @@ -464,3 +466,38 @@ worker cancellation, import limits, Cloud parity and the 1k/10k capacity gates remain open. The existing autosave behavior for newly laid-out or overlapping source items is outside this slice; zero-write navigation is tested on pinned, non-overlapping owned fixtures rather than asserted for every legacy graph. + + +## 9. Nested hierarchy continuation (2026-10-28, #215) + +The current candidate consumes every explicit path segment and normalized native +containment as a deterministic hierarchy, with display-only cuts at any supported +depth. Stable semantic path keys and canonical source IDs are separate. Children +replace a parent only as a complete partition; items directly in an expanded +parent have a separate direct-items representative. Cycles, multiple containers, +invalid segments and depth beyond 32 retain exact source coverage and expose +visible diagnostics. Every typed directed relationship remains internal, bundled +or pending its endpoint; self-links and parallel relationships are not discarded. + +The production group browser supports subgroup expansion, ancestor collapse and +complete original member inspection. Manual Overview starts at the roots. Auto +uses zoom hysteresis and the available non-overlapping screen-space label slots: +when all child representatives cannot fit, their named parent remains visible. +Selection stays attached to the original item and its current summary; Back +restores the entry camera and cut. Membership is memoized separately from camera +movement, and label slots stay fixed during a gesture within the same cut. + +This adapts CodeGraph's parent fallback, independent camera and stable hierarchy +ideas within GraphDone. It adds no shared library and changes no CodeGraph source. +It advances LOD-1/2 and the transition portion of LOD-3. It does **not** complete +per-region projected geometry/density, source DOM virtualization, relationship +inspection, AI import/export/MCP round trips, progressive loading, or hosted parity. +All source SVG nodes are still constructed, and graph data is fully loaded. Global +zoom bands and a label-slot budget are not evidence of measured 1k/10k capacity. + +Next bounded slices remain LOD-4 exact relationship exploration, LOD-5 portable +AI-authoring round trips, then LOD-3/6 region budgets and measured bounded rendering, +followed by LOD-7 authorized revision-aware loading and LOD-8 hosted acceptance. +The completion checklist in section 7 remains authoritative. See the +[nested validation ledger](../quality/NESTED_LOD_VALIDATION_2026-10-28.md) for +failing-before evidence, candidate checks and explicit remaining gates. diff --git a/docs/quality/NESTED_LOD_VALIDATION_2026-10-28.md b/docs/quality/NESTED_LOD_VALIDATION_2026-10-28.md new file mode 100644 index 00000000..0fecee8e --- /dev/null +++ b/docs/quality/NESTED_LOD_VALIDATION_2026-10-28.md @@ -0,0 +1,66 @@ +# Nested semantic LOD validation — 2026-10-28 + +Issue #215 continues the [delivery plan](../architecture/dynamic-graph-lod.md#9-nested-hierarchy-continuation-2026-10-28-215). +This ledger describes the candidate before final independent review and CI; +exact accepted revisions and final results belong in the implementation PR. + +## Source and environment + +The candidate starts at `53c124c39fd0e8495f26e27a981ad06557805878`, whose full tree +`fe1a54bc68148a2c5955c0d350784218c348d5f6` equals native dev merge +`77899e98a081e725ae8ddc15b6566cc50c9a584f`. It includes #213 layout recovery and +#214 self-host maintenance. Node 22.18.0/npm 10.8.2 clean install and Core build +succeeded. Owned Vite port 28928 uses the stable built API on 28817 and disposable +Neo4j 5.26.12 on bolt 28687. Original localhost 8540 and CodeGraph remain untouched. + +Browser coverage uses installed Chromium, one worker, zero retries, a desktop +1440×900 viewport and a 390×900 viewport with actual CDP touch events. This is +phone emulation, not physical-device acceptance. Fixtures register owned IDs +before setup completes and delete edges before nodes; teardown errors fail tests. + +## Failing-before and preliminary evidence + +On 2026-10-28, two new hierarchy assertions failed against the previous projection +(wrapper ignored the requested cut): explicit nested/direct membership and nested +native containment. The candidate passed those assertions and then all 18 model +cases, including unique coverage at every cut for 200 items/503 relationships, +reordered input, source object identity, cycles, parallel/opposite/self links, +missing endpoints, 2,000-deep native containment, 100-segment explicit paths, +invalid metadata, deterministic IDs, hysteresis and label-budget parent fallback. + +The new desktop journey also ran against the unchanged `168b984` web source on +28818. It failed at the missing `Show 2 subgroups` control with the existing 10s +action timeout, after authenticating and opening the real group browser. Trace +and screenshot were retained; the owned fixture was cleaned. This is an expected +negative control, not an accepted test run. + +The prototype's first desktop/phone run passed 2/2 in 2.3 minutes, using 72 pinned +items and 111 typed relationships with three-level paths, direct parent items, +cross-system cycles, parallel edges and a long member title. It checked expansion, +ancestor collapse, source inspection, exact return camera, actual pan and desktop +wheel input, unchanged label slots, exact source readback and zero attempted +mutations/page errors. Later assertions add explicit selected-parent accessibility +and repeated zoom-threshold crossings; they require fresh validation. + +Retained local evidence is under `/tmp/graphdone-resolution-20261027/`: +`nested-lod-negative.log`, `nested-lod-old-negative.log`, +`nested-lod-old-negative-artifacts/`, `nested-lod-budget-model.log`, +`nested-lod-first-browser.log` and `nested-lod-first-artifacts/`. +The strict `nested-lod` sequence is enrolled in PR/full profiles and the CI Smoke +Gate, with zero retries and retained failure traces. CLI tests verify enrollment. + +## Acceptance still required at the frozen candidate + +Run strict nested, existing 301/370 semantic LOD, layout recovery and smoke browser +sequences; existing mobile/new-user/performance gates; full unit/harness, build, +types, lint, audit, runtime images, real database contracts and installer gates. +Obtain independent exact-source review and inspect actual uploaded CI reports. +A passing prototype does not substitute for these gates or final dev CI. + +## Capability limits + +This adds nested display hierarchy and a screen-slot budget, not per-region +spatial virtualization or summary-first loading. All source SVGs/data remain +loaded. No 1k/10k capacity, hosted parity, topology clustering, production +publication or deployment is claimed. Existing source relationships and layout +recovery remain authoritative; summaries cannot be persisted as WorkItems. diff --git a/packages/web/src/components/SemanticGraphOverview.tsx b/packages/web/src/components/SemanticGraphOverview.tsx index 9c0e0d0e..4f2a17fb 100644 --- a/packages/web/src/components/SemanticGraphOverview.tsx +++ b/packages/web/src/components/SemanticGraphOverview.tsx @@ -2,7 +2,11 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react'; import { overviewAtScale, placeOverviewLabels, - projectSemanticGraph, + buildSemanticHierarchy, + projectSemanticHierarchy, + semanticDepthAtScale, + automaticSemanticExpansion, + overviewLabelCapacity, type SemanticEdge, type SemanticItem, } from '../lib/semanticLOD'; @@ -34,13 +38,70 @@ export function SemanticGraphOverview({ selectedId, onBrowse, }: Props) { - const projection = useMemo( - () => projectSemanticGraph(items, edges), + const hierarchy = useMemo( + () => buildSemanticHierarchy(items, edges), [items, edges] ); const host = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); + const bounds = host.current?.getBoundingClientRect(); + const obstacles = bounds + ? Array.from( + document.querySelectorAll( + '[data-graph-fit-obstacle="true"]' + ) + ) + .filter(element => element.getAttribute('role') !== 'region') + .map(element => element.getBoundingClientRect()) + .filter(rect => rect.width > 0 && rect.height > 0) + .map(rect => ({ + left: rect.left - bounds.left, + right: rect.right - bounds.left, + top: rect.top - bounds.top, + bottom: rect.bottom - bounds.top, + })) + : []; + const obstacleKey = JSON.stringify(obstacles); const [mode, setMode] = useState<'auto' | 'overview' | 'items'>('auto'); + const [expanded, setExpanded] = useState>( + () => new Set() + ); + const [automaticDepth, setAutomaticDepth] = useState(() => + semanticDepthAtScale(1, camera.scale) + ); + const expansionKey = useMemo( + () => + JSON.stringify( + [ + ...(mode === 'auto' + ? automaticSemanticExpansion( + hierarchy, + automaticDepth, + overviewLabelCapacity(size.width, size.height, obstacles) + ) + : expanded), + ] + .filter(id => hierarchy.groups.get(id)?.children.length) + .sort() + ), + [ + hierarchy, + mode, + automaticDepth, + expanded, + size.width, + size.height, + obstacleKey, + ] + ); + const effectiveExpanded = useMemo( + () => new Set(JSON.parse(expansionKey)), + [expansionKey] + ); + const projection = useMemo( + () => projectSemanticHierarchy(hierarchy, effectiveExpanded), + [hierarchy, effectiveExpanded] + ); const [automatic, setAutomatic] = useState(() => overviewAtScale(false, camera.scale) ); @@ -49,6 +110,7 @@ export function SemanticGraphOverview({ const [search, setSearch] = useState(''); const [canReturn, setCanReturn] = useState(false); const returnMode = useRef('auto'); + const returnExpanded = useRef>(new Set()); const closeButton = useRef(null); const backButton = useRef(null); const returnFocus = useRef(null); @@ -64,6 +126,7 @@ export function SemanticGraphOverview({ const group = projection.groups.find(candidate => candidate.id === groupId); useEffect(() => { setAutomatic(previous => overviewAtScale(previous, camera.scale)); + setAutomaticDepth(previous => semanticDepthAtScale(previous, camera.scale)); }, [camera.scale]); useEffect(() => { onActiveChange(active); @@ -103,24 +166,6 @@ export function SemanticGraphOverview({ y: y * camera.scale + camera.y, }; }); - const bounds = host.current?.getBoundingClientRect(); - const obstacles = bounds - ? Array.from( - document.querySelectorAll( - '[data-graph-fit-obstacle="true"]' - ) - ) - .filter(element => element.getAttribute('role') !== 'region') - .map(element => element.getBoundingClientRect()) - .filter(rect => rect.width > 0 && rect.height > 0) - .map(rect => ({ - left: rect.left - bounds.left, - right: rect.right - bounds.left, - top: rect.top - bounds.top, - bottom: rect.bottom - bounds.top, - })) - : []; - const obstacleKey = JSON.stringify(obstacles); // Keep slots fixed through camera gestures. Leaders still follow live source // anchors; only changed membership/viewport/controls triggers label placement. const layout = useMemo( @@ -154,6 +199,7 @@ export function SemanticGraphOverview({ const back = () => { closeBrowser(); setMode(returnMode.current); + setExpanded(returnExpanded.current); setCanReturn(false); onBack(); }; @@ -175,7 +221,9 @@ export function SemanticGraphOverview({ aria-label="Graph detail" value={mode} onChange={event => { - setMode(event.target.value as typeof mode); + const next = event.target.value as typeof mode; + if (next === 'overview') setExpanded(new Set()); + setMode(next); closeBrowser(); }} className="bg-gray-900 text-white" @@ -187,15 +235,30 @@ export function SemanticGraphOverview({ + {mode === 'overview' && effectiveExpanded.size > 0 && ( + + )} {canReturn && ( ); @@ -351,8 +429,55 @@ export function SemanticGraphOverview({

{group.members.length} items · {group.internalEdges.length}{' '} - internal relationships · grouped by {group.basis} + internal relationships · {group.blocked} blocked ·{' '} + {group.completed} completed · grouped by {group.basis}

+ {group.ancestors.length > 0 && ( + + )} + {group.children.length > 0 && ( + + )}