From 7e6531079bd083dc86609911f66c1a3b82cce368 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 08:10:38 -0700 Subject: [PATCH 01/14] docs: plan dynamic graph LOD through large-system delivery --- ...-graph-review-can-attempt-layout-writes.md | 72 ++++ ...27-snapshot-merges-are-not-page-appends.md | 56 +++ .agents/issues/INDEX.md | 3 + .agents/issues/performance-evidence-index.md | 2 + docs/README.md | 3 +- docs/USER_STORIES.md | 8 +- docs/architecture/dynamic-graph-lod.md | 405 ++++++++++++++++++ docs/design/progressive-streaming.md | 7 + 8 files changed, 553 insertions(+), 3 deletions(-) create mode 100644 .agents/inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md create mode 100644 .agents/inbox/2026-10-27-snapshot-merges-are-not-page-appends.md create mode 100644 docs/architecture/dynamic-graph-lod.md diff --git a/.agents/inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md b/.agents/inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md new file mode 100644 index 00000000..6beaf06c --- /dev/null +++ b/.agents/inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md @@ -0,0 +1,72 @@ +# Viewing a live graph can attempt layout writes even without dragging + +Date: 2026-10-27 UTC. Searched the current performance-evidence index and the +October 24 semantic-review worktree's symptom index, write-it-down guidance and +read-only capture lessons before recording this finding. + +## Evidence + +The existing harness at `01f0c4df1ba82eaaca9769d366886c76b99f0e6b` ran: + +```text +TEST_URL=http://127.0.0.1:8540 GRAPH_REVIEW_NAME='Graphlings Release' \ +OPERATOR_CREDENTIAL_FILE= \ +PLAYWRIGHT_CHANNEL=chrome \ +GRAPH_REVIEW_OUT=test-artifacts/graph-lod-review/continuation-20261027 \ +GRAPH_REVIEW_SNAPSHOT=1 npm run report:graph-lod + +finishedAt=2026-10-27T14:56:23.996Z +nodes=301 edges=370 captures=18 pageErrors=[] +blockedWrites=4 (POST /api/graphql) +1 failed: Review attempted a server write; request was blocked, inspect caller +``` + +No drag, seed or graph-edit interaction was requested. Node/edge identity +assertions passed. Desktop and phone fit screenshots still show no readable +titles or relationships. Served asset: `/assets/index-1788931697531.js`; its +exact source SHA is not established by the harness revision. + +A second bounded diagnostic installed API interception before login, permitting +queries and only the login mutation. It selected the same graph, waited, resized +to phone and fit the camera. It printed only operation/field names: + +```text +blocked=4 +operation=UpdateWorkItems +rootField=updateWorkItems +variableKeys=[where,update] +updateFields=[positionX,positionY] +``` + +That diagnostic exited 143 after printing; do not record it as a passing test. +The original sweep exited 1. Both failures remain explicit. Private screenshots +and the snapshot stay in the prior worktree's ignored artifacts directory. + +## Cause and corrective rule + +The application attempted automatic position persistence during a visual review. +Source has a `simulation.on('end.persist', ...)` → `persistAllPositions()` path; +the exact browser caller and affected graph were not captured, so that path is +a hypothesis, not a proved request stack. Do not attribute the request to zoom +alone or assume the selected graph was the mutation target. + +Install the read-only guard before application navigation, allowing only the +intended authentication exchange and reads on the configured API origins. +Retain safe operation/field names for rejected calls, never tokens, payload values +or session state. An attempted write remains a failed review even when blocked. +Reproduce on an owned fixture before correcting persistence intent. Camera, LOD +expansion and temporary summary coordinates must not enqueue saved positions. + +## Limits and regression path + +The first sweep installs its guard after login, so its blocked-write evidence +does not prove there were no earlier writes. Identity-set equality does not prove +position equality or whole-graph immutability. The diagnostic blocked the four +observed position updates, but did not establish root-cause completion. + +`tests/lib/graphReview.test.mjs` in the prior review branch passed 2/2 guard tests; +the real sweep supplies a failing acceptance case. LOD-0 in the +[continuation plan](../../docs/architecture/dynamic-graph-lod.md) ports and extends +that harness on current dev, with owned layout-persistence reproduction and +independent coordinate readback. No production fix or new passing regression is +claimed by this documentation-only change. diff --git a/.agents/inbox/2026-10-27-snapshot-merges-are-not-page-appends.md b/.agents/inbox/2026-10-27-snapshot-merges-are-not-page-appends.md new file mode 100644 index 00000000..a48336a1 --- /dev/null +++ b/.agents/inbox/2026-10-27-snapshot-merges-are-not-page-appends.md @@ -0,0 +1,56 @@ +# Snapshot replacement helpers must not consume individual graph pages + +Date: 2026-10-27 UTC. Searched the current symptom/performance indices, +`docs/design/progressive-streaming.md`, and the October semantic-LOD proposal. + +## Evidence + +At current Core dev `ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`: + +```text +packages/web/src/lib/graphDataMerge.ts +mergeSimulationNodes: + incomingIds = new Set(incoming.map(n => n.id)) + removedIds = simNodes.filter(n => !incomingIds.has(n.id)).map(n => n.id) + returned nodes are populated only from incoming +mergeSimulationEdges: + if (!sourceNode || !targetNode) { droppedIds.push(fresh.id); continue; } +``` + +The older streaming design says background pages can flow directly through these +identity-preserving helpers with no change. Source contradicts that assumption: +identity preservation for retained objects is not append semantics. + +The existing focused suites were also run in the prior semantic-review worktree +(`01f0c4d`) on 2026-10-27: + +```text +npm run test -w @graphdone/web -- \ + src/lib/__tests__/edgeLOD.test.ts \ + src/lib/__tests__/parallelEdges.test.ts \ + src/lib/__tests__/graphDataMerge.test.ts \ + src/lib/__tests__/adaptiveQuality.test.ts +4 files passed; 60 tests passed +``` + +The merge suite explicitly tests removed IDs and dropped missing-endpoint edges. +These tests verify the existing full-snapshot contract, not a streaming fix. + +## Cause and corrective rule + +The design conflated preservation of object identity with accumulation of partial +data. Introduce a canonical loaded-data registry with distinct snapshot/page/ +update/tombstone operations. Reconcile a page into that registry first. Pass the +reconciled loaded set to the D3 identity merge. Keep unresolved endpoints in a +pending-edge registry until loaded or explicitly deleted; do not equate unloaded, +filtered and deleted states. Bind pages to graph/access scope and revision. + +## Limits and regression path + +This is a source-level design incompatibility, not a reproduced production +streaming outage: streaming is not implemented. Preserve existing complete- +snapshot deletion semantics. LOD-1/LOD-7 in the +[continuation](../../docs/architecture/dynamic-graph-lod.md) require two-page +accumulation, delayed endpoint, explicit deletion, stale revision and graph/account +switch tests. No new runtime test is needed to claim this plan-only correction; +passing future streaming tests remains an implementation requirement. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 4e83a296..05269a21 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,9 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| Dense graph loses all names at fit scale; an old LOD preview is mistaken for shipped behavior | [Dynamic graph LOD continuation](../../docs/architecture/dynamic-graph-lod.md); current state, staged delivery and explicit completion gates | +| Read-only graph inspection attempts position writes without dragging | [Guard viewing before navigation and retain attempted-write failures](../inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md); existing capture guard failed correctly; owned reproduction planned | +| A proposed progressive page merge would erase earlier nodes or drop unloaded-endpoint edges | [Snapshot replacement is not page append](../inbox/2026-10-27-snapshot-merges-are-not-page-appends.md); existing merge tests establish snapshot semantics | | Deleting an owned folder removes or orphans another owner's nested folder; a deletion destination disappears with its parent | [Deletion authority includes pre-existing descendant folders](../inbox/2026-10-23-folder-delete-legacy-hierarchy.md); `packages/server/tests/folder-authorization.test.ts` | | Build/typecheck pass but API startup, supported-Node launcher, container or repository lint fails | [Startup and save evidence pointer](../inbox/2026-10-23-core-startup-and-save-evidence.md); [runtime validation](../../docs/quality/RUNTIME_VALIDATION_2026-10-23.md) | | Production accepts missing/default JWT or session secrets; `.env` key differs from the captured signing key | [Startup and save evidence pointer](../inbox/2026-10-23-core-startup-and-save-evidence.md); `packages/server/tests/production-startup.test.ts`, `production-secrets.test.ts` | diff --git a/.agents/issues/performance-evidence-index.md b/.agents/issues/performance-evidence-index.md index 23de511d..b231c8d3 100644 --- a/.agents/issues/performance-evidence-index.md +++ b/.agents/issues/performance-evidence-index.md @@ -2,5 +2,7 @@ | Symptom | Lesson and regression | | --- | --- | +| A completed visual capture reports attempted writes, or a fit graph has zero readable identities | [Live review write attempts](../inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md); [semantic LOD acceptance](../../docs/architecture/dynamic-graph-lod.md) | +| Partial loading is designed around complete-snapshot merge helpers | [Snapshot replacement is not page append](../inbox/2026-10-27-snapshot-merges-are-not-page-appends.md) | | Performance gate skips empty graphs, missing metrics, or times failed requests; mobile CI passes skipped required cases | [Required gates need owned measured evidence](../inbox/2026-10-23-performance-gate-evidence.md); `tests/lib/perf/*.test.mjs`, `tests/lib/runner/unified-cli.test.mjs`, `tests/perf/graph-perf.spec.ts` | | A placed graph renders but `window.__graphPerf` is absent, stale, or belongs to a previous graph | [First and terminal measurements must publish](../inbox/2026-10-23-performance-gate-evidence.md); the three-case required performance gate | diff --git a/docs/README.md b/docs/README.md index 6cd2896d..79e641cd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Welcome to the GraphDone documentation! This directory contains comprehensive gu - [Systems Reference](./SYSTEMS.md) - **What's shipped and where it lives** — every subsystem mapped to its code, tests, and story - [Interaction Model — the friction-free contract](./design/interaction-model.md) - UX constitution: modes, exits, click budgets - [Progressive Streaming design](./design/progressive-streaming.md) - ADAPT-4: scale to huge graphs on slow links +- [Dynamic graph LOD continuation](./architecture/dynamic-graph-lod.md) - Current evidence, semantic zoom, generic AI-authored systems, staged delivery and completion gates - [Testing & Refinement Plan](./TESTING_AND_REFINEMENT_PLAN.md) - The never-done loop; current cycle's verification debt - [AI Agents Quickstart](./api/AI_AGENTS.md) - 5-minute MCP/GraphQL setup for agent teammates @@ -92,4 +93,4 @@ Welcome to the GraphDone documentation! This directory contains comprehensive gu --- -*For teams who think differently* 🌐 \ No newline at end of file +*For teams who think differently* 🌐 diff --git a/docs/USER_STORIES.md b/docs/USER_STORIES.md index 842e40b4..83dc23dc 100644 --- a/docs/USER_STORIES.md +++ b/docs/USER_STORIES.md @@ -35,10 +35,14 @@ 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 thresholds parameterized by tier; snapshot tests per tier; no text rendering at far zoom on LOW. | `adaptiveQuality.test.ts` | 💤 | -| 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. | CI job fails if: web bundle gzip > 450kB, 500-node graph first-render > 1.5s in headless bench, dropped-frame rate > 20% in pan bench. | `tests/perf/check-bundle-size.mjs` + `graph-perf.spec.ts`, gated in CI | ✅ | +| 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-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` | ✅ | +The [dynamic graph LOD continuation](architecture/dynamic-graph-lod.md) specifies +the current ADAPT-4/ADAPT-7 delivery path. Its stage IDs are proposed PR-sized +work, not issued GitHub tickets or completed features. + ## Epic 3: Responsive Everywhere 📱💻 *Phone, tablet, PC — same living graph, appropriately shaped.* diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md new file mode 100644 index 00000000..53a828c8 --- /dev/null +++ b/docs/architecture/dynamic-graph-lod.md @@ -0,0 +1,405 @@ +# Dynamic graph LOD: continuation through delivery + +Reviewed 2026-10-27 UTC. Status: implementation plan, with fresh source, GitHub, +and localhost evidence. This document does not claim semantic LOD is implemented. +The stage IDs below are proposed work items, not existing GitHub issue numbers. + +The outcome is a graph that remains understandable while moving between an +entire system, its subsystems, a neighborhood, and an individual item. Zooming +out must replace detail with readable meaning. Zooming back in must recover +the same items, relationships, selection, and spatial context. + +The first delivery repairs the actual localhost use case. Completion also +requires generic AI-authored system graphs and a measured large-graph path; +the release-specific prototype alone cannot satisfy this feature. + +## 1. Reviewed state + +### Source and delivery inventory + +| Surface | Observed state | Consequence | +| --- | --- | --- | +| Core remote `dev` | `ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`, verified with `git ls-remote` | Base implementation work here, retaining the October save-recovery and performance-evidence fixes. | +| Core remote `main` | `775362eace0723c57b7ba3c7936645c9d0d36cb4` | Main is substantially older; do not start this work from its renderer. | +| Main promotion [PR #191](https://github.com/GraphDone/GraphDone-Core/pull/191) | OPEN; head `8a9c7512b5dedf4020d117887674d378de85667b`; `REVIEW_REQUIRED`, `BLOCKED` | A separate integration dependency for a main release, not a reason to stop LOD development on dev. Re-read state before integration. | +| Existing local semantic review | Branch `review/semantic-lod-20261024`, head `01f0c4df1ba82eaaca9769d366886c76b99f0e6b`; commits `fd72762` and `01f0c4d` add evidence, capture tooling, design and preview | Useful prior work, absent from remote dev. `git rev-list --left-right --count origin/dev...HEAD` there is `77 2`. Carry forward selected work onto current dev; do not build on the old runtime or overwrite new fixes. | +| Localhost application | `http://127.0.0.1:8540`, served asset `/assets/index-1788931697531.js` | The harness SHA is known; the deployed application's exact source SHA remains unproven. | +| Core rendering foundations | [PR #113](https://github.com/GraphDone/GraphDone-Core/pull/113), [PR #116](https://github.com/GraphDone/GraphDone-Core/pull/116), [PR #148](https://github.com/GraphDone/GraphDone-Core/pull/148), [PR #175](https://github.com/GraphDone/GraphDone-Core/pull/175) merged | Reuse edge fades/parallel offsets, layout, quality governor and strict measurement infrastructure. These are not semantic aggregation. | + +The four local repos were clean at initial inspection. Existing worktrees were +preserved; no intentional graph edits were performed. The old preview service was inactive; this +review did not restart or deploy it. Fleet discovery failed before yielding +context, so no current fleet execution ownership is claimed. + +### Current GitHub issues and how they affect this work + +Checked open issues and PRs through the GitHub connector and existing GitHub CLI +on 2026-10-27. The CLI also checked private GraphDone-Cloud, GraphDone-Devops and +GraphDone-LandingPage: each returned zero open issues and zero open PRs. An +organization-wide open-issue search returned the same nine Core issues below. +There is no open semantic/dynamic LOD issue in that inventory. References such +as “#22” in old PR prose are not a current LOD tracking issue. + +| Existing work | Relationship to this plan | +| --- | --- | +| [Core #181: unsent layout durability](https://github.com/GraphDone/GraphDone-Core/issues/181) | Coordinate saved-position behavior. Camera/aggregation must not create layout writes. Durable recovery of actual edits remains #181; do not represent local view-state persistence as fixing it. | +| [Core #182: hosted D1 acceptance](https://github.com/GraphDone/GraphDone-Core/issues/182) | Reuse its owned-browser, real readback, sync and account-isolation contract for hosted delivery. Core/Neo4j tests do not prove hosted behavior. | +| [Core #183](https://github.com/GraphDone/GraphDone-Core/issues/183), [#184](https://github.com/GraphDone/GraphDone-Core/issues/184), [#185](https://github.com/GraphDone/GraphDone-Core/issues/185), [#186](https://github.com/GraphDone/GraphDone-Core/issues/186) | Existing authentication/dependency work continues independently. Preserve its fixes and release disposition; it is not prerequisite work for pure LOD modules. | +| [Core #187: required checks](https://github.com/GraphDone/GraphDone-Core/issues/187) | Add LOD gates to existing CI/reporting and keep evidence failure-propagating. Branch-protection changes are separate work. | +| [Core #189: main promotion](https://github.com/GraphDone/GraphDone-Core/issues/189), [#190: release/deployment](https://github.com/GraphDone/GraphDone-Core/issues/190) | Source integration, registry publication and verified deployment are distinct steps. Do not close LOD delivery based only on a merge or a preview. | + +### Fresh localhost observation + +The existing read-only capture suite ran against **Graphlings Release** and +finished all 18 desktop/laptop/phone captures at `2026-10-27T14:56:23.996Z`. +The graph now has **301 nodes / 370 edges**, versus 299 / 370 in the October 24 +review. Compare identity sets, not just the two-node net increase. + +| Capture | Actual D3 scale | Readable individual titles (12px floor) | Painted edges | +| --- | ---: | ---: | ---: | +| 1600×1000 fit | 0.1090 | 0 | 0 | +| 1280×800 fit | 0.0844 | 0 | 0 | +| 390×844 fit | 0.0298 | 0 | 0 | +| 1600×1000 at 0.46 | 0.4600 | 0 | 135 | + +At 0.46, measured title text was 5.76–8.39 screen pixels. Desktop and phone +screenshots were inspected: both fit views are unlabeled collections of cards, +without readable semantic replacements. Preserved geometry alone is insufficient. + +**The sweep failed its read-only assertion.** Four GraphQL mutation attempts +were blocked, with no recorded page errors and unchanged node/edge identity sets. +A subsequent guarded diagnostic identified four `UpdateWorkItems` operations +whose update fields were `positionX` and `positionY`. It retained no credential, +payload values or session state; its process exited 143 after printing evidence, +so it is diagnostic output, not a passing test. The renderer's settle-persistence +path is a candidate cause, not an established request stack. The initial sweep +installs its guard after login; its evidence does not establish absence of writes +before that guard. The diagnostic guarded API traffic from before login, allowing +only queries and the login mutation. Do not remove the guard or call this a clean +read-only pass. Investigate on a disposable reproduction in LOD-0. + +Private local evidence remains in the prior review worktree: +`test-artifacts/graph-lod-review/continuation-20261027/{index.html,report.json,*.png,graph-snapshot.json}`. +Browser: Chrome 151.0.7922.71; harness Node 20.20.2. No rendering-speed benchmark +was performed by this capture. Public fixtures must use synthetic content. + +The existing review-guard unit suite passed **2/2**. Focused `edgeLOD`, +`parallelEdges`, `graphDataMerge` and `adaptiveQuality` suites passed **60/60** +in the prior review worktree. Those results establish the existing foundations; +they do not override the live sweep failure or validate future semantic LOD. +The four tested modules and four test files are byte-identical between that +branch and reviewed dev (`git diff --quiet` on the eight paths exited 0). +The refreshed snapshot contains 256 issues, 11 PRs, four milestones, 21 exit +criteria, eight goals and one gate; its edge types are 366 `DEPENDS_ON` and four +`BLOCKS`. Preserve type-specific denominators rather than calling every item an +issue or equating blocked status with a blocking relationship. + +### Source findings that change the implementation order + +1. `InteractiveGraphVisualization.tsx` uses a >150-node density threshold, + simplification below 0.45 and dots below 0.2. CSS hides node text and then + edges. Its camera, fit, culling, inspector and selection are reusable, but + there is no semantic hierarchy/projection between data and rendering. +2. `edgeLOD.ts` controls direction visibility, opacity and parallel offsets. + `shouldBundleEdges` is unit-tested but not imported by the main renderer. + Coincident same-pair lines are not count-preserving group-edge bundles. +3. `GET_WORK_ITEMS` fetches descriptions and nested relationships for the full + selected graph. The renderer supplies no paging options and polls nodes and + edges every minute. `maxInitialNodes` and `labelLodZoom` are declared quality + settings, not a working progressive-loading/semantic-detail implementation. +4. `mergeSimulationNodes` treats its input as a full snapshot: absent IDs become + removals. `mergeSimulationEdges` drops edges with unloaded endpoints. The + older [streaming design](../design/progressive-streaming.md) must be revised: + feeding pages straight into these helpers would lose previously loaded work. +5. `nodes` initialization scans edges for each node. Zoom callbacks walk SVG + selections and can measure title geometry. Profile these costs and replace + repeated scans with indices before choosing a new rendering engine. +6. `metadata` is fetched and survives object spreading, but shared `WorkItem` + typing lacks a normalized grouping contract. Graph/subgraph navigation is + distinct from grouping within one graph; an overview group is not a new + stored work item or permission boundary. +7. [Portable project/offline limits](../formats/PROJECT_TRANSFER.md) currently + allow 300 nodes / 600 links. Larger AI-generated imports therefore require + admission/import work even if Core's network-backed renderer can draw more. + Hosted capacity, pagination and persistence need a separate Cloud contract; + rendering optimization alone cannot establish larger hosted support. +8. The improved required performance suite measures four placed nodes and three + edges. It establishes valid instrumentation, not 1k/10k scalability. The + existing scale sweep is report-only; large-graph gates must become explicit. + +## 2. Product contract and scope + +| Detail | Visible representation | User can do | +| --- | --- | --- | +| System overview | Named subsystems/groups, scoped counts, status/exceptions, typed aggregated connections | Identify system areas, see important cross-area dependencies, choose a destination | +| Subsystem | Nested group boundaries and anchors; smaller groups remain summarized | Expand one area while retaining neighboring context; trace cross-area relationships | +| Neighborhood | Compact named items, selected one/two-hop relationships, offscreen destination summaries | Search, follow upstream/downstream links, select an exact item | +| Item detail | Existing card/inspector and full canonical relationship list | Read the exact title, description and provenance; perform existing authorized edits | + +Expose `Auto / Overview / Groups / Items` detail controls and a grouping selector. +Wheel/pinch remain spatial zoom; never silently change the app to a table. +Expansion overrides are user intent, while Auto adjusts the visible hierarchy +cut using projected size, local density, viewport obstacles and device budget. + +Selected items, keyboard focus, search results and inspected relationships stay +reachable at every scale. A collapsed ancestor carries the selection marker; +search expands/frames the path. Back/Escape restores the prior camera and cut. +Full source titles and IDs remain inspectable. Short labels are separate display +aliases, never destructive edits to imported names. + +The release graph supplies an initial metadata adapter. Generic graphs must +also work. Within the active lens, precedence is: authored grouping override, +explicit subsystem/group path, declared containment edges, configured metadata/ +tag grouping, then deterministic suggested topology groups. Normalize native +`IS_PART_OF` (child→parent) and `CONTAINS` (parent→child), including Grow's existing +`IS_PART_OF` output. Conflicting paths or multiple parents require a visible +diagnostic and a documented primary-parent choice/override; retain secondary +memberships and every original relationship. Suggestions +are labeled and stable until explicit rebuild or relevant data revision. A giant +“Unassigned” group alone is not completion for unannotated system graphs. Never +infer ownership or execution order from geometry or reinterpret `DEPENDS_ON` as +containment. Preserve `BLOCKS`, opposing directions, cycles and self-loops. + +The completion target proposed for this path is **10,000 source nodes / 50,000 +edges on a documented desktop profile**, with a 1,000 / 5,000 mobile gate and +bounded access to the larger graph through summaries. First useful delivery is +the real 301-node graph plus a synthetic equivalent, then a strict 1k gate. +100k+ is exploratory, not an implied unlimited-capacity promise. These are proposed +acceptance targets, not observed capabilities; ratify the hardware/budgets in +LOD-0 and retain failed measurements instead of quietly reducing the target. + +Release/milestone matrices, review queues, AI-written summaries and historical +diffs are useful follow-ups. They are not on the critical path for dynamic LOD. +Basic grouping, fallback for unannotated graphs, truthful relationships, navigation, +large-data access and persistence compatibility are on that path. + +## 3. Architecture decisions + +```mermaid +flowchart LR + A[Authorized graph data and revision] --> B[Normalized source registry] + B --> C[Stable grouping hierarchy] + C --> D[Visible hierarchy cut] + V[Camera, viewport, focus and quality budget] --> D + B --> E[Typed edge projection and accounting] + D --> E + E --> F[Bounded nodes, labels and connectors] + F --> G[Existing graph controls and inspector] +``` + +- **One source registry.** Retain canonical node/edge identity, metadata, loaded + state and revision outside D3 objects. Full snapshot replacement, page append, + update and deletion/tombstone are distinct operations. Unloaded is not deleted; + hidden/filtered is not unloaded. Resolve late endpoints without losing edges. +- **Stable grouping.** A pure `graphGrouping` module produces scoped group keys, + parent/child membership, display names and diagnostics. Each source node has + one primary owner per active lens; overlapping tags are secondary facets. + Detect missing parents, cycles, duplicate keys and depth overflow. Preserve + affected nodes in visible diagnostic/unassigned groups. Rebuild on relevant + data/lens changes, not wheel events. +- **Count-preserving projection.** Pure `graphProjection` maps each authorized, + in-scope node to a visible representative. Aggregate edges by source + representative, target representative, type and direction. Keep underlying + IDs addressable; internal edges become inspectable counts. Use an exclusive + accounting ledger: rendered individual, aggregated, internal, explicitly + filtered, or pending endpoint. Viewport culling is a presentation attribute, + not another source-edge category. No silent loss or double counting. +- **One LOD policy.** `semanticLOD` consumes projected dimensions/density and + existing adaptive-quality signals. Quality may lower effect/primitive budgets, + but it must not erase the last readable group identity. Replace conflicting + thresholds gradually; retain edgeLOD helpers where useful at item detail. +- **Readable labels.** A screen-space label layer with deterministic priority + and collision handling; group titles ≥14px and item names ≥12px. Budget names + by available space (initially 24–40 desktop, 6–10 phone). Reserve focus/search + and blocker labels; expose overflow explicitly. Obstacles include headers, + floating controls, inspector and minimap. Verify hit-testing as well as bounds. +- **Stable motion.** Start with 15–20% hysteresis and 150–250ms crossfades, tuned + against real wheel/pinch input. Freeze expensive membership computation during + a gesture. One representation owns input during transitions; reduced motion + uses immediate stable swaps. Keep the zoom anchor stable across expansion. +- **Separate coordinates and writes.** Summary anchors and temporary expanded + layout are view coordinates. Preserve saved item positions and manual pins. + Zoom, fit, collapse, filters and view restoration enqueue no node-position + mutation. Drag/explicit layout commands retain the existing save queue and + error handling. Synthetic summary IDs can never reach CRUD/undo mutations. +- **Scoped view state.** Versioned camera/lens/expansion/selection state keyed + by owner + graph, with pruning on deletion and permission loss. Use local view + persistence first; portable saved views require explicit format support. + It does not replace the pending-edit durability work in #181. +- **Bound work, not just opacity.** Render only the chosen projection, with + overscan. Cache indices and aggregates; coalesce camera changes once per + animation frame. Run costly grouping/layout in a cancellable worker if needed; + reject stale results after graph/account/lens changes. Avoid force reheats on + zoom. Retain SVG for the first useful slice. Decide Canvas/WebGL from profiles + after projection; preserve accessible DOM controls and hit-testing if changed. + +## 4. Ordered delivery backlog + +Every stage is a reviewable PR or small PR series. Tests below extend existing +Vitest/Playwright/unified-report infrastructure. Keep stages open until their +acceptance evidence exists; the names do not imply an issue has been created. + +| Stage / proposed title | Deliverable and boundary | Depends on | Exit evidence | +| --- | --- | --- | --- | +| **LOD-0 — Establish the current semantic-zoom baseline** | Port the useful October capture helpers onto current dev without old runtime code or private captures. Add a synthetic release-shaped fixture and a generic subsystem fixture. Add complete read-only request guarding and safe operation diagnostics; reproduce the position-write attempt on owned data. Pin hardware, target graph identity and budgets. | None | Missing-data, attempted-write and missing-metric negative controls fail. Desktop/phone baseline retained. Diagnose or explicitly isolate pre-existing layout autosave before declaring a clean live review. | +| **LOD-1 — Normalize grouping and conserve graph identity** | Typed metadata adapter, stable hierarchy and projection modules; graph revision and exact/partial count semantics. Support release lanes and generic nested group paths. Build directed/type-aware aggregate edges from the outset. | LOD-0 fixture contracts | Unit/property tests conserve nodes/edges, survive bad metadata/cycles, preserve antiparallel and mixed-type edges, and round-trip expand/collapse. | +| **LOD-2 — Ship a readable overview in the real renderer** | Render named summary primitives and basic typed connectors; scoped counts, blocker indicators, group click/keyboard focus, breadcrumb and back. Retain the existing detail renderer and inspector. Feature flag permits comparison/rollback. | LOD-1 | Real localhost and synthetic release-shaped overview have readable groups and all explicit blocking relationships discoverable at fit scale; summary drill-down reaches exact items; no saved-position writes during navigation. | +| **LOD-3 — Make detail continuous and locally adaptive** | Screen-space labels, compact items, local density/quality budgets, hysteresis, transition ownership, selection/search retention, inspector-aware fit, touch and keyboard behavior. | LOD-2 | Repeated 0.44↔0.46 and 0.19↔0.21 crossings produce no anonymous interval, focus loss or uncontrolled card explosion. Dense and sparse regions can use different representations. All label/hit-target floors pass. | +| **LOD-4 — Complete relationship exploration** | Exact bundle drill-down, internal-edge inspection, focused 1–2 hops, upstream/downstream convention, named offscreen ports, complete accessible relation list and filtering denominators. | LOD-2; integrate with LOD-3 | Every source edge is accounted for; selected paths remain traceable through collapse; exact IDs/types/directions return from bundle expansion; cycles/self-loops/100+ degree hubs are navigable. | +| **LOD-5 — Support AI-authored systems and unknown schemas** | Document an optional versioned grouping/display metadata contract and an AI-authoring example. Preserve canonical titles/provenance. Validate import/export/MCP/API round trips. Add deterministic suggested grouping for graphs without metadata; rename/pin/split/merge overrides survive updates. | LOD-1; UI uses LOD-3/4 | A nested software system and an unannotated topology graph both support overview→detail→back. Imported IDs/group references remap correctly. Malformed AI payloads retain recoverable data and explain errors. No inference/remote summarization runs on zoom; bounded, coalesced and cancellable graph-data requests may serve a newly visible region. | +| **LOD-6 — Enforce 1k scale and bounded rendering** | Source-registry caching, adjacency/spatial indices, projected DOM budget, metrics for projection/labels/frame latency, cancellable workers where profiling requires. Remove repeated full-graph scans from gesture work. | LOD-3/4; generic fixtures from LOD-5 | Required 1k/5k-edge desktop and mobile journeys pass with exact source counts and no sentinel metrics, retries or required skips. Publish CPU/layout/paint traces and before/after measurements. | +| **LOD-7 — Load and navigate 10k graphs without full hydration** | Replace the obsolete streaming assumptions with an authorized overview/neighborhood/page contract; bounded first paint, summary-first loading, cancellable requests, stable cursors/revision, pending endpoints and server-side search. Integrate import/storage admission limits. | LOD-1 source semantics; full UI on LOD-6 | End-to-end 10k/50k graph loads a truthful overview, searches an unloaded item, expands its region and follows edges without downloading all descriptions first. Missing/delayed/duplicate/stale pages cannot erase data or claim completeness. | +| **LOD-8 — Prove cross-backend compatibility** | Cloud-owned adapter and capacity policy; align authorized summary/page/search contracts, Core submodule pin, offline/import behavior, metadata round trips and bounded persistence. Preserve conservative public guest admission separately from larger supported accounts/operators. | LOD-5/7; #182 acceptance infrastructure | Owned Core and hosted/staging browser journeys return equivalent memberships, relationships and selection. Real persistence/account-isolation readback; visible limit errors and recovery. Exact frontend/API/schema versions recorded. | +| **LOD-9 — Release and close the feature** | Reconcile current base, complete review/gates, document controls/limits, rollout flag, verify installed artifacts and rollback. Move the canonical design/story status with the implementation. | LOD-0…8; applicable #189/#190 path | All definition-of-done rows below satisfied on the delivered build; remaining enhancements separated. Merge/preview alone cannot close this stage. | + +The shortest first-useful path is **LOD-0 → LOD-1 → LOD-2**. The next milestone is +**LOD-3 + LOD-4 + LOD-5 → LOD-6**. API contract design for LOD-7 can proceed once +LOD-1 establishes identity/count semantics; the completion gate remains after +the integrated UI. Cloud planning can start early without changing deployment. +Avoid a wholesale rewrite of the 6k-line renderer as a prerequisite: introduce +the projection boundary, then extract the layers touched by each slice. + +### The first implementation PR + +Start from current dev, rechecking upstream and active ownership. Bring the +read-only review helper and its tests forward with source/build provenance; +keep the static walkthrough as a reference artifact. Add sanitized fixtures, +normalization/grouping types and conservation tests. Extend shared WorkItem +typing rather than hiding metadata parsing in SVG callbacks. Keep runtime +behavior behind the existing renderer boundary until LOD-1 invariants pass. + +The first renderer PR then consumes that projection for overview + basic +relationships + drill-down. Do not spend this first slice on the release matrix, +automatic prose summaries, a WebGL migration, or a fresh test platform. + +## 5. Large-data and AI authoring contracts + +The June streaming proposal's offset/dedup sketch is insufficient for mutable +large graphs and exact totals. Specify an authorized, revision-aware API in +LOD-7 (names such as `graphOverview`, `graphNeighborhood`, `graphPage` are proposed): + +1. Overview returns group identity/hierarchy, exact authorized counts, bounded + typed aggregate connections, freshness/revision and completeness status. + It must not fetch every long description merely to compute the overview. +2. Neighborhood/member pages return stable IDs, cursor/tie-breaker and revision; + cancel obsolete requests on graph/lens/account change. A stale page cannot + replace a newer edit or expand a graph the user has left. Concurrent edits + require revision restart/reconciliation, not a false “all loaded” badge. +3. Server-side search returns exact matches plus their grouping path and a way + to fetch the selected item; client-only search cannot cover unloaded data. +4. Source storage explicitly models pending endpoint edges and deletions. Reuse + D3 identity merge on the reconciled loaded set, never on a single page. +5. Counts distinguish total authorized, filter-matched, loaded and visible. + Partial counts are labeled. Do not derive a total from the current page or + leak inaccessible node existence through summary counts or edge endpoints. +6. Authorization filters precede aggregation. Cache keys include access scope, + graph revision and grouping/filter schema. Revoked access clears the affected + registry, view state and pending requests. Test cross-account negative cases. +7. Query cost, response bytes, cache size, concurrency and import size are bounded. + Respect Save-Data/low tiers; expansion works on demand. Error/offline/loading + states retain existing context and do not become “empty graph.” + +For AI-produced graphs, begin with a documented optional namespace inside the +existing metadata envelope: schema version, stable semantic group path/key, +display alias and provenance. Final field names are part of LOD-1/5 review. +Unknown metadata is preserved. Primary membership is deterministic; multiple +lenses do not multiply counts. Group references must use portable semantic keys +or be explicitly remapped with fresh node IDs on import. The current opaque +metadata string does not automatically remap embedded node references. + +Provide one validated JSON example and one API/MCP creation example for a +system→service→component hierarchy with cross-service typed edges, cycles, +unassigned items and long names. Confirm the actual importer and server support +those contracts; merely documenting a payload is not acceptance. Any schema +change must retain existing version-1 import/export compatibility or offer an +explicit versioned migration. Large import limits must be reconciled across UI, +offline store, portable validation and backend admission. + +Core owns portable semantics, renderer, agent authoring contract and tests. +Cloud owns hosted storage/admission, adapter parity and deployment evidence. +Devops changes are needed only if packaging/observability requirements emerge; +LandingPage changes follow demonstrated capability and documented limits. + +## 6. Validation and performance gates + +Extend `edgeLOD`, `parallelEdges`, `graphDataMerge`, `adaptiveQuality`, adjacency +and projection unit suites. Add a dedicated semantic-LOD browser suite within +the current graph/quality projects and report manifest, using existing seed, +auth, cleanup, screenshot and diagnostic helpers. Port the live review as opt-in +read-only evidence; never run mutating scale fixtures against the user's graph. + +| Layer | Required cases | +| --- | --- | +| Pure invariants | Unique node coverage; exclusive edge accounting; stable IDs/order under reordered input; nested/cyclic/missing membership; self/parallel/opposite edges; internal counts; filter denominators; expansion round trip; partial pages and deletions | +| Small regression | 0/1/12/50/150/151 nodes; existing create/edit/delete/grow/drag/undo, subgraph navigation, saved positions and small-graph readability | +| Real shape | Sanitized prior 299-node shape and refreshed 301-node shape; all source entities and blocking relationships preserved; long imported titles, pinned lanes, hubs and supporting entity types | +| Generic systems | Nested explicit subsystems; containment-only IS_PART_OF/CONTAINS hierarchy; conflicting metadata/containment and multiple parents; metadata-free communities; disconnected islands; dense/sparse areas together; multilingual/long labels; high-degree hub; cyclic typed dependencies | +| Scale | 500/1k/2k/10k nodes with recorded edge counts/density; 1k/5k and 10k/50k strict targets; placed and unplaced layouts measured separately | +| Interaction | Fit→expand→select→collapse→return; search in collapsed/unloaded group; repeated threshold crossing; pan to offscreen port; resize/inspector; wheel/pinch/tap/keyboard; reduced motion; browser zoom and device-pixel ratio | +| Live updates | Added/removed/moved items, metadata changes, delayed acknowledgements, deleted selection, stale worker/page response, account switch/revocation, graph switch during expansion | +| Persistence | Zero node writes for navigation; real edits still use save recovery; owner-scoped view restoration; import/export identity mapping; no synthetic summaries persisted as nodes | + +Proposed acceptance budgets, to freeze in LOD-0 on named hardware/browser: + +- Readable summary titles ≥14 screen pixels; item labels ≥12; ordinary touch + targets ≥44px. No overlap/occlusion of required labels or pointer interception. + Test actual visible text and ordinary clicks, not DOM existence or forced clicks. +- At 1k/5k, p95 input-to-next-frame <50ms; aim for ≤16.7ms steady desktop pan + frames and ≤33.3ms on the mobile profile. No >100ms main-thread task caused by + an LOD transition. Report median/p95/worst, not only average FPS. +- At 10k/50k on the desktop profile, p95 input-to-next-frame <50ms after overview + availability, using bounded projection; do not relax readability to meet time. + Record overview load, detail load, API time, transfer bytes, DOM primitives, + projection/label time, heap and retained cache separately. +- Preserve the ADAPT-4 target of useful first paint <2s for 1k on explicitly + configured simulated 3G; define latency/bandwidth/cache settings in the fixture. + For 10k, proposed useful overview <3s on the documented normal-network profile. + “Useful” requires readable summaries and counts, not a spinner or first dot. +- Repeated full zoom/expand/collapse/graph-switch cycles must plateau in retained + DOM/cache/heap after cleanup. Set concrete memory caps from the LOD-0 profiles; + no numeric heap limit is claimed as measured by this review. +- Existing strict smoke, mobile, layout/durability, unit/harness and required CI + gates stay enabled. Required LOD cases have zero skips/retries/flaky passes; + missing or stale metrics fail. Keep failure evidence and fixture cleanup results. + +Retain exact source/lockfile, served build identity, backend/schema/graph revision, +browser/hardware/viewport/DPR, fixture seed and edge density, flags, network/CPU +settings and projection counts with results. FPS from this workstation does not +establish phone performance. The current host is Ryzen 9 3900X / 24 logical CPUs; +headed GPU rendering and headless/software rendering must be labeled separately. + +Run task-based human checks at the 301-node and generic-system milestones: +identify a subsystem within 10 seconds, locate a blocker and inspect its source +within 30 seconds, follow a cross-subsystem dependency, and return without losing +place. Record correct/incorrect answers and navigation failures; screenshots +and geometry checks alone cannot establish comprehension. + +## 7. Definition of done and rollout + +- [ ] Real localhost and generic AI-authored graphs remain named and navigable at + every detail band; automatic zoom and manual overrides agree. +- [ ] Selection/search/keyboard/touch and typed relationships survive every + collapse/expand/filter boundary; exact source data remains inspectable. +- [ ] All node/edge accounting, metadata/import and partial-loading invariants pass. +- [ ] The 1k/mobile and 10k/desktop targets pass on documented profiles, including + first useful load and bounded rendering, rather than only a cached static demo. +- [ ] Navigation changes no saved positions; actual edits retain existing recovery + guarantees. LOD does not silently weaken #181 or import/offline compatibility. +- [ ] Hosted supported limits and behavior are explicit and tested with real + account-isolated persistence. Any hosted large-capacity gap remains open, + rather than being hidden by a Core-only success. +- [ ] Source review and current required gates pass; released/installed frontend + and backend versions are verified. Documentation, stories and regression index + are updated with actual evidence and remaining limits. +- [ ] Rollback is proven: disable the feature or restore the prior tested build + without rewriting source graph data or destroying saved user layouts. + +Roll out behind a developer/test flag, validate owned fixtures, then conduct a +guarded review of the real localhost graph. Enable for supported large graphs +after acceptance; retain the manual detail control and accessible relation list. +Ship the exact reviewed Core revision through each installation's existing +release path. Recheck #191/#189/#190 rather than assuming their October status. + +The next action is LOD-0/LOD-1 preparation on current dev, followed by the first +production overview. This plan is complete as a planning artifact; the feature +remains open until the checklist above has evidence on the delivered application. diff --git a/docs/design/progressive-streaming.md b/docs/design/progressive-streaming.md index 03417af7..3da58982 100644 --- a/docs/design/progressive-streaming.md +++ b/docs/design/progressive-streaming.md @@ -3,6 +3,13 @@ > Status: design accepted (2026-06-11). Implementation next. > Story: ADAPT-4 in [USER_STORIES.md](../USER_STORIES.md). Budget: TTFP < 2s on simulated 3G for a 1k-node graph. +> **2026-10-27 review:** implementation remains pending. The current merge helpers +> consume complete snapshots, remove absent nodes and drop edges with unloaded +> endpoints; passing each page directly to them is unsafe. Follow the +> [dynamic LOD continuation](../architecture/dynamic-graph-lod.md) for the revised +> source-registry, summary-first loading, revision/cursor and completeness +> contracts. The mechanics below are historical design, not shipped behavior. + ## Problem The graph view fetches **every** work item and edge in the graph in one query. At 1k+ nodes on a slow link this blows the time-to-first-paint budget, and most of those nodes land outside the initial viewport anyway. Quality tiers already bound how much we *render* (`maxInitialNodes`); nothing bounds what we *fetch*. From 73a574b28124107b7b29cc2757e08bf50590838a Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 08:20:51 -0700 Subject: [PATCH 02/14] docs: reuse CodeGraph techniques in the dynamic LOD plan --- ...-lod-reuse-needs-live-consumer-evidence.md | 49 +++++++ .agents/issues/INDEX.md | 1 + docs/README.md | 1 + docs/architecture/codegraph-lod-reuse.md | 136 ++++++++++++++++++ docs/architecture/dynamic-graph-lod.md | 13 ++ 5 files changed, 200 insertions(+) create mode 100644 .agents/inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md create mode 100644 docs/architecture/codegraph-lod-reuse.md diff --git a/.agents/inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md b/.agents/inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md new file mode 100644 index 00000000..e4b8d3f1 --- /dev/null +++ b/.agents/inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md @@ -0,0 +1,49 @@ +# Reuse decisions need consumer evidence as well as a passing library test + +Date: 2026-10-27 UTC. Searched GraphDone's semantic/performance lessons and +CodeGraph's current unified-viewer plan before updating the LOD continuation. + +The owner identified prior CodeGraph LOD work omitted from the first GraphDone +plan. Review found useful implemented behavior in the mandala and modern +continuous Structure workspace, plus a tempting standalone LOD module whose +comments describe broader sharing than its consumers establish. + +## Receipt + +At Graphlings source `c5fb0089f4672d1d33c4fbf6c54bef63feab2636`: + +```text +node tools/codegraph/src/codegraph/assets/lib/lod.js --test +lod.js self-test: OK + +rg -n 'lib/lod|window\.LOD|LOD\.(swap|band|hysteresis|labelAlpha|fadeIn)' \ + tools/codegraph/src/codegraph/assets +.../assets/lib/lod.js:72:if (typeof window !== "undefined") window.LOD = LOD; + +tools/codegraph/docs/UNIFIED-VIEWER-PLAN.md: +records that lod.js has no viewer consumers and enlighten uses inline LOD + +git diff --name-only -- tools/codegraph + +``` + +The active inline hierarchy walk retains a parent when estimated children would +be too small; `placeLabels` prioritizes folders and collision-culls names. +`structure-workspace.js` separately maintains stable page/world identity, +bounded requests and generation checks. Those are the stronger reuse references. +Some legacy browser controls still expect mandala globals at the new Structure +front door. The installed `/api/runtime` returned 503; no live viewer pass or +performance guarantee was established in this review. + +## Cause, corrective rule and limits + +The first plan considered GraphDone in isolation. The follow-up also illustrates +why a file's name/header and passing pure tests do not establish runtime adoption. +Before calling code shared, trace imports and actual consumers, then verify the +behavior contract in each. Reuse observed algorithms and test invariants first; +extract a minimal common module only with two wired consumers and an appropriate +licensing boundary. Do not transfer historical FPS claims or stale test routes. + +Canonical guidance: [CodeGraph reuse map](../../docs/architecture/codegraph-lod-reuse.md). +This change updates the plan and its discovery links. It copies no proprietary +source, changes no renderer, and does not claim the integration is implemented. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 05269a21..d0f31d3b 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| A new LOD design overlooks CodeGraph or treats an unused helper as a shipped shared engine | [CodeGraph reuse map](../../docs/architecture/codegraph-lod-reuse.md); [consumer-evidence lesson](../inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md) | | Dense graph loses all names at fit scale; an old LOD preview is mistaken for shipped behavior | [Dynamic graph LOD continuation](../../docs/architecture/dynamic-graph-lod.md); current state, staged delivery and explicit completion gates | | Read-only graph inspection attempts position writes without dragging | [Guard viewing before navigation and retain attempted-write failures](../inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md); existing capture guard failed correctly; owned reproduction planned | | A proposed progressive page merge would erase earlier nodes or drop unloaded-endpoint edges | [Snapshot replacement is not page append](../inbox/2026-10-27-snapshot-merges-are-not-page-appends.md); existing merge tests establish snapshot semantics | diff --git a/docs/README.md b/docs/README.md index 79e641cd..129b6ef6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ Welcome to the GraphDone documentation! This directory contains comprehensive gu - [Interaction Model — the friction-free contract](./design/interaction-model.md) - UX constitution: modes, exits, click budgets - [Progressive Streaming design](./design/progressive-streaming.md) - ADAPT-4: scale to huge graphs on slow links - [Dynamic graph LOD continuation](./architecture/dynamic-graph-lod.md) - Current evidence, semantic zoom, generic AI-authored systems, staged delivery and completion gates +- [CodeGraph LOD reuse](./architecture/codegraph-lod-reuse.md) - Working techniques, source references and regression contracts to share with GraphDone - [Testing & Refinement Plan](./TESTING_AND_REFINEMENT_PLAN.md) - The never-done loop; current cycle's verification debt - [AI Agents Quickstart](./api/AI_AGENTS.md) - 5-minute MCP/GraphQL setup for agent teammates diff --git a/docs/architecture/codegraph-lod-reuse.md b/docs/architecture/codegraph-lod-reuse.md new file mode 100644 index 00000000..2641e482 --- /dev/null +++ b/docs/architecture/codegraph-lod-reuse.md @@ -0,0 +1,136 @@ +# Sharing LOD techniques between CodeGraph and GraphDone + +Reviewed 2026-10-27 following the owner's direction to reuse prior CodeGraph work. +This supplements the [dynamic LOD delivery plan](dynamic-graph-lod.md). The first +implementation should start from the mechanisms and regression contracts below, +rather than inventing another zoom policy or loading lifecycle. + +## Source and evidence boundary + +CodeGraph lives in `Valpatel/graphlings/tools/codegraph`. Reviewed remote dev and +local HEAD: `c5fb0089f4672d1d33c4fbf6c54bef63feab2636`. The local Graphlings tree +has unrelated changes; `git diff --name-only -- tools/codegraph` returned no +paths. Those changes were left intact. Source links below pin this revision. + +The newer default Structure workspace is the strongest model for continuous +hierarchical loading. The legacy mandala still contains useful LOD/label/card +logic. They are different renderers; do not combine their claims into an +imaginary already-shared engine. + +`node tools/codegraph/src/codegraph/assets/lib/lod.js --test` returned +`lod.js self-test: OK`. However, source searches found no current viewer import +of that module. The server can serve it, but the mandala uses its own inline +`LOD` settings and functions. The [unified viewer plan][viewer-plan] explicitly +records this wiring gap. A passing pure self-test does not prove viewer use. + +The installed service's `/api/runtime` returned HTTP 503 during this review. +No live CodeGraph usability/performance pass is claimed. Browser tests below +were inspected, not executed. Some legacy LOD tests still navigate `/` or +`/#structure` while expecting mandala globals; the default route now hosts the +continuous workspace. Port their invariants with correct routes and fixtures, +not their selectors or historical pass counts. + +## Reuse map + +| Existing technique and source | GraphDone application | Adaptation required | +| --- | --- | --- | +| **Projected-size hierarchy walk** in [mandala `enlighten.html`][mandala], around `walk`, `childRk` and `drawDirNode` | LOD-1/2: retain a named parent until its children can actually appear. Decide locally per group, not solely from global zoom or total node count. | The circle-pack estimate `parent radius / sqrt(child count)` is specific to that geometry. For GraphDone cards/pinned layouts, measure projected child bounds and label occupancy. Never expand into an unreadable or unloaded empty region. | +| **Parent-first collision-aware labels**, `placeLabels` and `labelAlphaFor` in [mandala][mandala] | LOD-3: keep navigation identities readable, then admit item names according to available space. | GraphDone adds focus/search/blocker priorities, measured text bounds, panel obstacles and stable tie-breaking. Retain its proposed 14px group / 12px item floors; CodeGraph's 10px/9px floors and 600-label cap are not suitable defaults to copy. | +| **Separate enter/exit thresholds and bounded rich cards** in [mandala][mandala] | LOD-3: avoid flickering representations; limit expensive detail panels while keeping ordinary compact items accessible. | Existing card thresholds are 80/58 projected radius, max four transient cards, with selection work on a 180ms cadence. Reuse the state-machine/cost separation, not those exact values. Camera motion stays frame-synchronous; expensive detail admission can be coalesced. | +| **Pure fade/band/swap/hysteresis functions** in [`lib/lod.js`][lod] | Candidate common primitive for transition math and contract tests across both apps. | It is not currently wired into viewers. Extract/adapt live behavior first and compare outputs before wiring; opacity conservation alone does not ensure readable labels or correct pointer ownership. | +| **Stable world coordinates while pages arrive**, `layoutChild`/`acceptTree` in [Structure workspace][structure] | LOD-1/7: append data without moving already-visible anchors; keep graph identity separate from page state. | CodeGraph assigns a child slot using stable index and total before all pages arrive. GraphDone must preserve authored positions and semantic group anchors; do not re-tile the user's saved graph or mistake unloaded items for deletions. | +| **Bounded, coalesced, cancellable loading**, `enqueueTree`/`pump`/`lazyLoad` in [Structure workspace][structure] | LOD-7, with the contract established in LOD-1: load visible detail only, cancel obsolete scope, reject stale results. | CodeGraph uses 100-entry pages, two requests, a 140ms lazy-load delay, AbortControllers, scope/project epochs and response-generation tokens. Bind GraphDone jobs to owner + graph + revision + grouping/filter lens; keep unloaded-edge endpoints pending. | +| **One retained renderer/camera**, `worldAt`/`screen`, pointer gestures, visibility messages and `requestDraw` in [Structure workspace][structure] | LOD-2/3: overview→subsystem→item remains one spatial world; preserve pointer anchor and camera on mode return; pause hidden work. | Keep GraphDone's existing D3 camera/inspector integration initially. Do not transplant the CodeGraph shell, iframe routing or filesystem UI. Canvas plus accessible DOM targets is a reference if profiling later warrants a hybrid renderer. | +| **Generation-bound identity and explicit incomplete coverage** in [repository views][views] and Structure `subject`/inventory handling | LOD-1/7/8: distinguish missing data, partial results, stale data and actual deletion; do not show a different graph under the selected label. | A CodeGraph token binds an indexed commit/capture, not GraphDone authorization. Preserve account/ACL boundaries before aggregates and adapt to mutable graph revisions. Offset pages are safe only under an equivalent stable-snapshot contract. | +| **Seeded topology grouping with fallback** in [navigator `_detect_communities`][navigator] | LOD-5: a concrete starting experiment for suggested grouping when explicit containment/metadata is absent. | CodeGraph uses weighted Louvain on an undirected view, resolution 0.5 and seed 42, with connected-component fallback. These code-graph weights/parameters are not a universal system ontology. Evaluate GraphDone fixtures, stabilize group IDs across updates and preserve original directed/typed edges independently. | + +GraphDone's existing `graphAdjacency`, `edgeLOD`, `graphDataMerge`, quality governor, +camera and save queue remain foundations. CodeGraph's directory tree does not +provide GraphDone's full arbitrary-graph projection: multiple membership, +conflicting containment, exact typed/directed bundles, filters and edit recovery +still need GraphDone-specific contracts. + +## Regression techniques worth sharing first + +The strongest immediate transfer is the test intent, before any runtime package: + +| CodeGraph control | GraphDone counterpart | +| --- | --- | +| [`test_lod_descent_no_dead_zone.py`][no-dead-zone] | Sample intermediate zoom steps in both directions; require a readable group/item representation and navigable context at each sample. Strengthen “something drawn” to meaningful semantic coverage. | +| [`test_folder_label_lod_climb.py`][label-climb] | Dense views may cull names, but continued zoom must eventually reveal each requested group/item. Use a discriminating intermediate density, not a sparse endpoint that passes despite a broken label budget. | +| `test_lod_all_files_load.py` and `test_lod_dense_folder_all_files_load.py` | Every member remains reachable on descent, including dense groups and unusual entity types. Assert exact membership, not just a nonzero rendered count. | +| [`test_portfolio_view.py::test_structure_directory_paging_adds_nodes_without_moving_existing_world`][portfolio-tests] | Hold the next page, capture existing IDs/world coordinates, release it, assert new nodes append while prior anchors stay fixed. Include pending endpoints and explicit deletions. | +| `test_structure_wheel_pan_and_fit_keep_one_canvas`, real multitouch and mode-return controls in [portfolio tests][portfolio-tests] | Check actual wheel/pinch/pan, renderer identity, zoom anchor and camera restoration. A URL change or screenshot count does not establish continuous navigation. | +| Late-source/foreign-generation/unavailable-inventory controls in [portfolio tests][portfolio-tests] | A delayed response cannot replace a newer graph, selected item or account. Failed/incomplete inventory retains honest context; it does not become an empty graph or confirmed removal. | + +Run these in GraphDone's existing Playwright/Vitest/report stack using owned +synthetic fixtures. Keep CodeGraph's Python/browser harness in its repo. Shared +fixture data and invariant definitions can have adapters for both harnesses; +there is no need to import one application's entire test infrastructure. + +## Sharing boundary and delivery order + +1. **LOD-0: use these regression contracts now.** Add dense-child visibility, + label self-resolution, stable-page geometry and camera-identity checks to the + first fixture plan. Record the CodeGraph source revision for each borrowed + technique. Correct routes before reusing a legacy browser control. +2. **LOD-1/2: adapt the actual hierarchy walk and source registry.** Implement + GraphDone's pure cut/projection boundary using parent fallback and stable + anchors. Define page accumulation/epochs early, even while the first slice + still receives a complete graph. This avoids rewriting the boundary in LOD-7. +3. **LOD-3: consolidate genuinely common primitives.** Candidates are projected + size bands, hysteresis state, label ordering/box collision and camera transform + math. A small renderer-neutral JavaScript/TypeScript interface can consume + geometry/intent and return decisions; it should know nothing about React, + D3, Neo4j, filesystem paths or repository tokens. Supply policy/budgets through + adapters. Use behavior-comparison tests against the live inline implementation + before replacing it. Do not call it shared until both consumers are wired. +4. **LOD-7: adapt the modern request lifecycle.** Keep browser request scheduling + reusable in concept; application-specific transport/auth/response validation + stays in each adapter. Add GraphDone's mutable-data and pending-edge semantics + rather than copying CodeGraph's immutable directory API. +5. **Then evaluate a common package.** Require two real consumers, attribution, + versioned interface, tests and packaging without service dependencies. Reusing + techniques and contracts is useful immediately; a new package is not a + prerequisite for delivering the first GraphDone overview. + +CodeGraph's package declares `LicenseRef-Valpatel-Proprietary`; GraphDone-Core +is MIT. This review copies no implementation into Core and publishes nothing. +Any source extraction into a distributed shared package or public Core needs an +explicit licensing choice; technique reuse and project-specific implementation +can proceed without silently relicensing private source. + +Useful return flow to CodeGraph: GraphDone's exact typed-edge accounting, +partial-count semantics, selection through aggregation, graph edit protection, +and generic hierarchy-conflict tests should become cross-project contract cases. +That would improve both products rather than leave two unrelated LOD forks. + +## Current related CodeGraph work + +Checked on GitHub during this review: + +- [#2764](https://github.com/Valpatel/graphlings/issues/2764), all-project zoom and + usable graph panels, remains open; the continuous Structure implementation + landed via [#2802](https://github.com/Valpatel/graphlings/pull/2802). Source + presence and an open umbrella issue are different evidence. +- [#2803](https://github.com/Valpatel/graphlings/issues/2803), retained local work + in the same world, remains open. Current source/docs contain local-provider + handling; do not infer deployed coverage or closeout from that alone. +- [#2808](https://github.com/Valpatel/graphlings/issues/2808) records an oversized + edge target making a whole Cosmos projection unavailable, including affected + GraphDone repos. Carry the lesson into GraphDone: bounded partial projection + must disclose omitted edges without truncating identifiers or inventing links. + +The immediate recommendation is to borrow **parent fallback, progressive label +resolution, stable camera/world identity and generation-bound loading**, with +their tests. Keep GraphDone's domain semantics and existing renderer integration. + +[mandala]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/src/codegraph/assets/enlighten.html +[lod]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/src/codegraph/assets/lib/lod.js +[structure]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/src/codegraph/assets/lib/structure-workspace.js +[navigator]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/src/codegraph/navigator.py +[views]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/docs/REPOSITORY-VIEWS.md +[viewer-plan]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/docs/UNIFIED-VIEWER-PLAN.md +[no-dead-zone]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/tests/visual/test_lod_descent_no_dead_zone.py +[label-climb]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/tests/visual/test_folder_label_lod_climb.py +[portfolio-tests]: https://github.com/Valpatel/graphlings/blob/c5fb0089f4672d1d33c4fbf6c54bef63feab2636/tools/codegraph/tests/visual/test_portfolio_view.py diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md index 53a828c8..79bd03d2 100644 --- a/docs/architecture/dynamic-graph-lod.md +++ b/docs/architecture/dynamic-graph-lod.md @@ -4,6 +4,12 @@ Reviewed 2026-10-27 UTC. Status: implementation plan, with fresh source, GitHub, and localhost evidence. This document does not claim semantic LOD is implemented. The stage IDs below are proposed work items, not existing GitHub issue numbers. +The owner's CodeGraph reuse direction is incorporated in the +[cross-project technique and source map](codegraph-lod-reuse.md). Use the working +mandala hierarchy/label logic and modern continuous Structure loading lifecycle +as implementation references. CodeGraph's standalone `lod.js` is not currently +wired into its viewers; do not mistake its self-test for production integration. + The outcome is a graph that remains understandable while moving between an entire system, its subsystems, a neighborhood, and an individual item. Zooming out must replace detail with readable meaning. Zooming back in must recover @@ -403,3 +409,10 @@ release path. Recheck #191/#189/#190 rather than assuming their October status. The next action is LOD-0/LOD-1 preparation on current dev, followed by the first production overview. This plan is complete as a planning artifact; the feature remains open until the checklist above has evidence on the delivered application. + +CodeGraph-informed refinement: LOD-0 includes intermediate-zoom coverage, label +self-resolution and stable-page/camera tests. LOD-1 adopts parent fallback and +explicit page accumulation/epochs from the outset. LOD-3 evaluates extraction of +shared geometry/transition primitives with tests against both consumers; LOD-7 +adapts generation-bound, coalesced loading. Domain-specific edge projection and +GraphDone edit/persistence behavior remain explicit responsibilities. From df5d1cab932985092d77c17826af9d5cfc0e94b0 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 09:08:05 -0700 Subject: [PATCH 03/14] feat: add readable semantic overview to the graph camera --- ...lod-projections-need-renderer-ownership.md | 66 +++ .agents/issues/INDEX.md | 1 + .agents/issues/performance-evidence-index.md | 1 + docs/architecture/codegraph-lod-reuse.md | 45 +- docs/architecture/dynamic-graph-lod.md | 52 +- .../InteractiveGraphVisualization.tsx | 96 +++- .../src/components/SemanticGraphOverview.tsx | 462 ++++++++++++++++++ packages/web/src/index.css | 9 + packages/web/src/lib/semanticLOD.test.ts | 197 ++++++++ packages/web/src/lib/semanticLOD.ts | 276 +++++++++++ packages/web/src/types/graph.ts | 3 +- tests/e2e/graph/semantic-lod.spec.ts | 439 +++++++++++++++++ tests/lib/seedGraph.ts | 11 +- 13 files changed, 1607 insertions(+), 51 deletions(-) create mode 100644 .agents/inbox/2026-10-27-lod-projections-need-renderer-ownership.md create mode 100644 packages/web/src/components/SemanticGraphOverview.tsx create mode 100644 packages/web/src/lib/semanticLOD.test.ts create mode 100644 packages/web/src/lib/semanticLOD.ts create mode 100644 tests/e2e/graph/semantic-lod.spec.ts diff --git a/.agents/inbox/2026-10-27-lod-projections-need-renderer-ownership.md b/.agents/inbox/2026-10-27-lod-projections-need-renderer-ownership.md new file mode 100644 index 00000000..4c8f3331 --- /dev/null +++ b/.agents/inbox/2026-10-27-lod-projections-need-renderer-ownership.md @@ -0,0 +1,66 @@ +# Display projections need explicit renderer ownership and real selection checks + +Date: 2026-10-27. Repository: GraphDone-Core. Base: current dev +`ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`. Related implementation: +`semanticLOD.ts`, `SemanticGraphOverview.tsx`, `InteractiveGraphVisualization.tsx`. +Existing guidance searched: symptom index, performance-evidence index, the live +graph write-attempt lesson and the snapshot-merge lesson. + +## Evidence and cause + +The first nine projection/placement tests passed, but independent review found +that a sibling summary overlay bypassed the existing SVG's native gestures and +overlapped Workspace controls. Moving it inside the retained SVG fixed gesture +ownership, but exposed a second boundary: descendant `svg.select('defs')` could +select React's nested definitions instead of the source renderer's definitions. +Source children also override inherited visibility/pointer-events, so hiding only +their parent with those properties did not reliably disable source hit targets. + +The second headed browser run used an owned Neo4j fixture, 301 pinned items and +370 relationships, at widths 1440 and 390: + +```text +TEST_URL=http://127.0.0.1:28528 QC_BROWSER_PORT=28529 xvfb-run --auto-servernum \ + npx playwright test tests/e2e/graph/semantic-lod.spec.ts \ + --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line +2 failed: getByTestId('node-inspector') expected visible; element(s) not found +``` + +The item camera moved, but the member click removed its browser section before +the document outside-click handler inspected the target. A `closest()` exclusion +alone failed because the target's detached subtree no longer reached the overview +root. Stop click propagation at the React overview boundary, while allowing native +camera gestures to reach the owning SVG. The third run passed both viewports, +including inspector selection, Back, wheel, zero attempted writes and exact saved +position readback before/after navigation and reload. + +Earlier validation failures are retained in ignored +`test-artifacts/lod-evidence/first-attempt/` and `second-attempt/`. The first run +also had fixture confounders: a concurrent typecheck rebuilt Core and restarted +the watched API; 100-input Neo4j create mutations exceeded the test timeout. +Use the built API without a watcher and repeated 20-input mutation shapes for +this fixture. Keep the full 301/370 dataset. The default parallel web unit run +had a 16ms timing assertion fail and a 5s UI timeout; a one-worker run passed all +335 tests. Do not run timing-sensitive validation alongside heavy fixture work. + +## Corrective rule + +- Keep source and projected objects separate; projected coordinates never enter + simulation merges or the save queue. +- Give React and D3 explicit sibling roots. Scope D3 definition selection to + direct children and preserve the React root during initialization. +- Hide source groups with `display:none` while the overview owns interaction; + refresh text fitting once they become measurable again. +- Exercise ordinary pointer, keyboard and touch navigation through the actual + production component. Assert the inspector and focus, not only camera motion + or a `data-active` flag. Check forced Overview at normal item scale too. +- Bind Back state to an inspection session, including graph changes, and retain + exact typed/directed edge accounting even when some labels cannot fit. + +## Limits + +These are local owned-fixture results, not evidence of deployment or Cloud parity. +This slice retains the full source SVG and full graph queries. It does not resolve +legacy autosave of newly placed/overlapping nodes, nested spatial cuts, streaming, +or the 1k/10k performance targets. A local note is not published knowledge until +the related reviewed change is integrated. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index d0f31d3b..8ccd0c5e 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| LOD inspection moves the camera but loses selection; hidden source controls or nested SVG definitions leak across renderers | [Display projections need explicit renderer ownership](../inbox/2026-10-27-lod-projections-need-renderer-ownership.md); `semantic-lod.spec.ts`, `semanticLOD.test.ts` | | A new LOD design overlooks CodeGraph or treats an unused helper as a shipped shared engine | [CodeGraph reuse map](../../docs/architecture/codegraph-lod-reuse.md); [consumer-evidence lesson](../inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md) | | Dense graph loses all names at fit scale; an old LOD preview is mistaken for shipped behavior | [Dynamic graph LOD continuation](../../docs/architecture/dynamic-graph-lod.md); current state, staged delivery and explicit completion gates | | Read-only graph inspection attempts position writes without dragging | [Guard viewing before navigation and retain attempted-write failures](../inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md); existing capture guard failed correctly; owned reproduction planned | diff --git a/.agents/issues/performance-evidence-index.md b/.agents/issues/performance-evidence-index.md index b231c8d3..e456a015 100644 --- a/.agents/issues/performance-evidence-index.md +++ b/.agents/issues/performance-evidence-index.md @@ -2,6 +2,7 @@ | Symptom | Lesson and regression | | --- | --- | +| A summary screenshot passes but selection/gestures fail, or a watched API restarts during a timing gate | [Projection ownership and stable owned-fixture validation](../inbox/2026-10-27-lod-projections-need-renderer-ownership.md); production `semantic-lod.spec.ts` | | A completed visual capture reports attempted writes, or a fit graph has zero readable identities | [Live review write attempts](../inbox/2026-10-27-live-graph-review-can-attempt-layout-writes.md); [semantic LOD acceptance](../../docs/architecture/dynamic-graph-lod.md) | | Partial loading is designed around complete-snapshot merge helpers | [Snapshot replacement is not page append](../inbox/2026-10-27-snapshot-merges-are-not-page-appends.md) | | Performance gate skips empty graphs, missing metrics, or times failed requests; mobile CI passes skipped required cases | [Required gates need owned measured evidence](../inbox/2026-10-23-performance-gate-evidence.md); `tests/lib/perf/*.test.mjs`, `tests/lib/runner/unified-cli.test.mjs`, `tests/perf/graph-perf.spec.ts` | diff --git a/docs/architecture/codegraph-lod-reuse.md b/docs/architecture/codegraph-lod-reuse.md index 2641e482..ad5f486d 100644 --- a/docs/architecture/codegraph-lod-reuse.md +++ b/docs/architecture/codegraph-lod-reuse.md @@ -37,7 +37,7 @@ not their selectors or historical pass counts. | **Projected-size hierarchy walk** in [mandala `enlighten.html`][mandala], around `walk`, `childRk` and `drawDirNode` | LOD-1/2: retain a named parent until its children can actually appear. Decide locally per group, not solely from global zoom or total node count. | The circle-pack estimate `parent radius / sqrt(child count)` is specific to that geometry. For GraphDone cards/pinned layouts, measure projected child bounds and label occupancy. Never expand into an unreadable or unloaded empty region. | | **Parent-first collision-aware labels**, `placeLabels` and `labelAlphaFor` in [mandala][mandala] | LOD-3: keep navigation identities readable, then admit item names according to available space. | GraphDone adds focus/search/blocker priorities, measured text bounds, panel obstacles and stable tie-breaking. Retain its proposed 14px group / 12px item floors; CodeGraph's 10px/9px floors and 600-label cap are not suitable defaults to copy. | | **Separate enter/exit thresholds and bounded rich cards** in [mandala][mandala] | LOD-3: avoid flickering representations; limit expensive detail panels while keeping ordinary compact items accessible. | Existing card thresholds are 80/58 projected radius, max four transient cards, with selection work on a 180ms cadence. Reuse the state-machine/cost separation, not those exact values. Camera motion stays frame-synchronous; expensive detail admission can be coalesced. | -| **Pure fade/band/swap/hysteresis functions** in [`lib/lod.js`][lod] | Candidate common primitive for transition math and contract tests across both apps. | It is not currently wired into viewers. Extract/adapt live behavior first and compare outputs before wiring; opacity conservation alone does not ensure readable labels or correct pointer ownership. | +| **Pure fade/band/swap/hysteresis functions** in [`lib/lod.js`][lod] | Reference for independently implemented transition math and regression cases in each app. | It is not currently wired into viewers. Extract/adapt live behavior first and compare outputs before wiring; opacity conservation alone does not ensure readable labels or correct pointer ownership. | | **Stable world coordinates while pages arrive**, `layoutChild`/`acceptTree` in [Structure workspace][structure] | LOD-1/7: append data without moving already-visible anchors; keep graph identity separate from page state. | CodeGraph assigns a child slot using stable index and total before all pages arrive. GraphDone must preserve authored positions and semantic group anchors; do not re-tile the user's saved graph or mistake unloaded items for deletions. | | **Bounded, coalesced, cancellable loading**, `enqueueTree`/`pump`/`lazyLoad` in [Structure workspace][structure] | LOD-7, with the contract established in LOD-1: load visible detail only, cancel obsolete scope, reject stale results. | CodeGraph uses 100-entry pages, two requests, a 140ms lazy-load delay, AbortControllers, scope/project epochs and response-generation tokens. Bind GraphDone jobs to owner + graph + revision + grouping/filter lens; keep unloaded-edge endpoints pending. | | **One retained renderer/camera**, `worldAt`/`screen`, pointer gestures, visibility messages and `requestDraw` in [Structure workspace][structure] | LOD-2/3: overview→subsystem→item remains one spatial world; preserve pointer anchor and camera on mode return; pause hidden work. | Keep GraphDone's existing D3 camera/inspector integration initially. Do not transplant the CodeGraph shell, iframe routing or filesystem UI. Canvas plus accessible DOM targets is a reference if profiling later warrants a hybrid renderer. | @@ -52,7 +52,7 @@ still need GraphDone-specific contracts. ## Regression techniques worth sharing first -The strongest immediate transfer is the test intent, before any runtime package: +The strongest immediate transfer is the test intent: | CodeGraph control | GraphDone counterpart | | --- | --- | @@ -64,9 +64,8 @@ The strongest immediate transfer is the test intent, before any runtime package: | Late-source/foreign-generation/unavailable-inventory controls in [portfolio tests][portfolio-tests] | A delayed response cannot replace a newer graph, selected item or account. Failed/incomplete inventory retains honest context; it does not become an empty graph or confirmed removal. | Run these in GraphDone's existing Playwright/Vitest/report stack using owned -synthetic fixtures. Keep CodeGraph's Python/browser harness in its repo. Shared -fixture data and invariant definitions can have adapters for both harnesses; -there is no need to import one application's entire test infrastructure. +synthetic fixtures. Keep CodeGraph's Python/browser harness in its repo. Translate +fixture shapes and invariant definitions into each harness independently. ## Sharing boundary and delivery order @@ -78,32 +77,22 @@ there is no need to import one application's entire test infrastructure. GraphDone's pure cut/projection boundary using parent fallback and stable anchors. Define page accumulation/epochs early, even while the first slice still receives a complete graph. This avoids rewriting the boundary in LOD-7. -3. **LOD-3: consolidate genuinely common primitives.** Candidates are projected - size bands, hysteresis state, label ordering/box collision and camera transform - math. A small renderer-neutral JavaScript/TypeScript interface can consume - geometry/intent and return decisions; it should know nothing about React, - D3, Neo4j, filesystem paths or repository tokens. Supply policy/budgets through - adapters. Use behavior-comparison tests against the live inline implementation - before replacing it. Do not call it shared until both consumers are wired. -4. **LOD-7: adapt the modern request lifecycle.** Keep browser request scheduling - reusable in concept; application-specific transport/auth/response validation - stays in each adapter. Add GraphDone's mutable-data and pending-edge semantics - rather than copying CodeGraph's immutable directory API. -5. **Then evaluate a common package.** Require two real consumers, attribution, - versioned interface, tests and packaging without service dependencies. Reusing - techniques and contracts is useful immediately; a new package is not a - prerequisite for delivering the first GraphDone overview. - -CodeGraph's package declares `LicenseRef-Valpatel-Proprietary`; GraphDone-Core -is MIT. This review copies no implementation into Core and publishes nothing. -Any source extraction into a distributed shared package or public Core needs an -explicit licensing choice; technique reuse and project-specific implementation -can proceed without silently relicensing private source. +3. **LOD-3: apply the ideas independently.** Projected size bands, hysteresis, + label ordering/box collision and camera math inform GraphDone's own modules. + Compare observable behavior with the live CodeGraph implementation; keep + application policy and tests in their respective repositories. +4. **LOD-7: adapt the modern request lifecycle.** Implement GraphDone's own + bounded scheduling, cancellation and revision checks, including mutable-data + and pending-edge semantics. + +The owner clarified on 2026-10-27: **no shared libraries**. Exchange techniques, +regression lessons and design findings in both directions. Do not create a common +package, extract CodeGraph source, or couple either application's release cycle. +This review uses ideas; each project keeps its own implementation and harness. Useful return flow to CodeGraph: GraphDone's exact typed-edge accounting, partial-count semantics, selection through aggregation, graph edit protection, -and generic hierarchy-conflict tests should become cross-project contract cases. -That would improve both products rather than leave two unrelated LOD forks. +and generic hierarchy-conflict tests can inform CodeGraph's own regression cases. Record those opportunities without creating a shared dependency. ## Current related CodeGraph work diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md index 79bd03d2..40537dab 100644 --- a/docs/architecture/dynamic-graph-lod.md +++ b/docs/architecture/dynamic-graph-lod.md @@ -406,13 +406,57 @@ after acceptance; retain the manual detail control and accessible relation list. Ship the exact reviewed Core revision through each installation's existing release path. Recheck #191/#189/#190 rather than assuming their October status. -The next action is LOD-0/LOD-1 preparation on current dev, followed by the first -production overview. This plan is complete as a planning artifact; the feature +The first implementation below advances LOD-1/2 on current dev. The feature remains open until the checklist above has evidence on the delivered application. CodeGraph-informed refinement: LOD-0 includes intermediate-zoom coverage, label self-resolution and stable-page/camera tests. LOD-1 adopts parent fallback and -explicit page accumulation/epochs from the outset. LOD-3 evaluates extraction of -shared geometry/transition primitives with tests against both consumers; LOD-7 +explicit page accumulation/epochs from the outset. LOD-3 applies geometry/transition ideas in GraphDone's own implementation; LOD-7 adapts generation-bound, coalesced loading. Domain-specific edge projection and GraphDone edit/persistence behavior remain explicit responsibilities. + +Owner clarification (2026-10-27): reuse ideas in both directions with CodeGraph; +keep implementations independent. No shared libraries or common package. + +## 8. First production-renderer slice (2026-10-27) + +`packages/web/src/lib/semanticLOD.ts` now projects loaded source items into +top-level groups. Precedence is explicit `metadata.lod.groupPath` (or +`metadata.groupPath`), native containment, release `lane`, `kind`, then item type. +Metadata may be a JSON string or an object. For example: + +```json +{"lod":{"groupPath":["Payments","Settlement workers"]}} +``` + +This initial cut displays **Payments**; nested spatial expansion is still pending. +Containment directions are normalized, cycles terminate deterministically, and +multiple-parent/conflicting-path diagnostics remain visible in the group list. +Every loaded relationship is accounted for as an internal link, a typed directed +bundle, or a pending-endpoint edge. Canonical items/edges are retained unchanged. + +`SemanticGraphOverview` is integrated into `InteractiveGraphVisualization`, not a +standalone preview. Graphs with at least 40 loaded items offer Auto, Overview and +Items. Auto retains summaries through a hysteresis band. Group labels use fixed +14px screen-space text, avoid current graph controls, and keep their slots during +camera gestures. Leaders connect labels to source centroids; arrows show bundled +directions. The group browser supplies exact typed counts and full member titles. +Groups that cannot fit on a phone remain available through the complete group +list. Inspecting a member selects the original item in the existing inspector; +Back restores the entry camera and detail mode. + +The summary layer is a React-owned SVG `foreignObject` inside the existing D3 +camera surface. Native wheel, mouse and touch gestures use the existing zoom +behavior. D3 owns only its sibling graph layers and direct-child definitions. +Source layers are removed from painting/hit testing while Overview is active; +projected coordinates never enter the source simulation or position-save queue. + +This is a useful **overview and navigation slice**, not completion of LOD-2/3. +It still loads and constructs the full source SVG. The next implementation work +is a nested spatial cut with stable group anchors, continuous readable transitions, +selected/search-path expansion, per-region density budgets, and typed relationship +inspection. Type fallback is explicit, not claimed topology clustering. Streaming, +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. diff --git a/packages/web/src/components/InteractiveGraphVisualization.tsx b/packages/web/src/components/InteractiveGraphVisualization.tsx index a3e3cba5..49c7be7b 100644 --- a/packages/web/src/components/InteractiveGraphVisualization.tsx +++ b/packages/web/src/components/InteractiveGraphVisualization.tsx @@ -1,6 +1,7 @@ import { skipBackgroundPoll } from '../lib/hostedUsage'; import { QuickEditSurface } from './QuickEditSurface'; -import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { SemanticGraphOverview } from './SemanticGraphOverview'; import ReactDOM from 'react-dom/client'; import { createPortal } from 'react-dom'; import * as d3 from 'd3'; @@ -656,7 +657,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i if (!svgRef.current) return; const svg = d3.select(svgRef.current); - const defs = svg.select('defs'); + const defs = svg.select(':scope > defs'); const nodeTypeConfig = getTypeConfig(node.type as WorkItemType); const nodeColor = nodeTypeConfig.hexColor; @@ -675,7 +676,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i if (!svgRef.current) return; const svg = d3.select(svgRef.current); - const defs = svg.select('defs'); + const defs = svg.select(':scope > defs'); // Remove glow and reset properties from all elements first svg.selectAll('.node-bg').style('filter', null); @@ -982,7 +983,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i // Clicks inside the docked inspector (a sibling tree) must not deselect // the node — otherwise its own Card/Contents/Diagram controls close it. const target = event.target as Element | null; - if (target && target.closest('[data-testid="node-inspector"], [role="dialog"]')) { + if (target && target.closest('[data-testid="node-inspector"], [role="dialog"], .semantic-overview')) { return; } setNodeMenu(prev => ({ ...prev, visible: false })); @@ -1144,12 +1145,13 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i } }, [currentGraph?.id, refetch, refetchEdges]); - const workItemEdges: WorkItemEdge[] = []; + const workItemEdges = useMemo(() => { + const result: WorkItemEdge[] = []; // Add edges from Neo4j Edge entities if (edgesData?.edges) { edgesData.edges.forEach((edge: any) => { - workItemEdges.push({ + result.push({ id: edge.id, source: edge.source.id, target: edge.target.id, @@ -1168,7 +1170,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i workItems.forEach(item => { if (item.dependencies) { item.dependencies.forEach((dep: WorkItem) => { - workItemEdges.push({ + result.push({ id: `edge-${dep.id}-${item.id}`, source: item.id, target: dep.id, @@ -1181,6 +1183,9 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i }); } + return result; + }, [edgesData, workItems]); + // Validate and sanitize data before D3 processing const currentValidationResult = validateGraphData(workItems, workItemEdges); @@ -1356,7 +1361,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i if (isFirstInit) { // First initialization - clear everything and create core structure - svg.selectAll('*').remove(); + svg.selectAll(':scope > :not(.semantic-overview-host)').remove(); d3.select(containerRef.current).selectAll('.node-labels-container').remove(); } else { // Surgical update - only clear data elements, preserve core structure @@ -1373,6 +1378,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i // Create zoom behavior for empty canvas with reversed wheel direction const zoom = d3.zoom() + .filter(event => !(event.target as Element)?.closest('[data-lod-controls]') && (!event.ctrlKey || event.type === 'wheel') && !event.button) .scaleExtent([0.1, 3]) .wheelDelta((event: WheelEvent) => { // Standard direction (like maps/most apps): scroll UP (deltaY<0) zooms IN. @@ -1385,7 +1391,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i } }); - const g = isFirstInit ? svg.append('g').attr('class', 'main-graph-group') : existingMainGroup; + const g = isFirstInit ? svg.insert('g', '.semantic-overview-host').attr('class', 'main-graph-group') : existingMainGroup; // Create grid pattern and overlay (only on first init) if (isFirstInit) { @@ -1820,7 +1826,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i if (isFirstInit) { // First initialization - clear everything and create core structure - svg.selectAll('*').remove(); + svg.selectAll(':scope > :not(.semantic-overview-host)').remove(); d3.select(containerRef.current).selectAll('.node-labels-container').remove(); } else { // Surgical update - only clear data elements, preserve core structure @@ -1841,13 +1847,14 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i // Create or reuse zoom behavior with reversed wheel direction const zoom = d3.zoom() + .filter(event => !(event.target as Element)?.closest('[data-lod-controls]') && (!event.ctrlKey || event.type === 'wheel') && !event.button) .scaleExtent([0.1, 4]) .wheelDelta((event: WheelEvent) => { // Standard direction (like maps/most apps): scroll UP (deltaY<0) zooms IN. return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002); }); - const g = isFirstInit ? svg.append('g').attr('class', 'main-graph-group') : existingMainGroup; + const g = isFirstInit ? svg.insert('g', '.semantic-overview-host').attr('class', 'main-graph-group') : existingMainGroup; // Create or reuse background for capturing clicks let background = g.select('rect.background'); @@ -2278,7 +2285,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i }); // Create or get defs element for filters - let filterDefs = svg.select('defs') as any; + let filterDefs = svg.select(':scope > defs') as any; if (filterDefs.empty()) { filterDefs = svg.append('defs') as any; } @@ -4268,6 +4275,48 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i const simulationRef = useRef | null>(null); useEffect(() => () => stopActiveSimulation(), [stopActiveSimulation]); const zoomBehaviorRef = useRef | null>(null); + const [semanticOverviewActive, setSemanticOverviewActive] = useState(false); + const handleOverviewActive = useCallback((active: boolean) => { + setSemanticOverviewActive(active); + if (active) setSelectedNode(null); + }, []); + const closeOverviewInspector = useCallback(() => setSelectedNode(null), []); + const overviewReturnCamera = useRef<{ graphId: string; transform: d3.ZoomTransform } | null>(null); + const readOverviewPositions = useCallback(() => simulationRef.current?.nodes() ?? [], []); + const inspectOverviewItem = useCallback((id: string, beginSession: boolean) => { + const item = simulationRef.current?.nodes().find(node => node.id === id); + if (!item || !svgRef.current || !containerRef.current || !zoomBehaviorRef.current || !currentGraphIdRef.current) return; + if (beginSession || overviewReturnCamera.current?.graphId !== currentGraphIdRef.current) { + overviewReturnCamera.current = { graphId: currentGraphIdRef.current, transform: d3.zoomTransform(svgRef.current) }; + } + // Selection is intentional here; never invoke the Grow/connect click path. + setSelectedNode(item); + setSelectedNodes(new Set([id])); + setEditingEdge(null); + cameraManuallyPositionedRef.current = true; + const k = 1.2; + const transform = d3.zoomIdentity.translate(containerRef.current.clientWidth / 2 - item.x * k, containerRef.current.clientHeight / 2 - item.y * k).scale(k); + d3.select(svgRef.current).interrupt().call(zoomBehaviorRef.current.transform, transform); + }, []); + const returnToOverview = useCallback(() => { + setSelectedNode(null); + const saved = overviewReturnCamera.current; + if (!saved || saved.graphId !== currentGraphIdRef.current || !svgRef.current || !zoomBehaviorRef.current) return; + d3.select(svgRef.current).interrupt().call(zoomBehaviorRef.current.transform, saved.transform); + overviewReturnCamera.current = null; + }, []); + const previousOverview = useRef({ active: false, graphId: currentGraph?.id }); + useEffect(() => { + // Source title fitting needs measurable elements after display:none is lifted. + // Do not synthesize a camera event on initial load or across graph scopes: + // that would persist an unfitted/default (or previous graph's) camera. + const previous = previousOverview.current; + previousOverview.current = { active: semanticOverviewActive, graphId: currentGraph?.id }; + if (previous.active && previous.graphId === currentGraph?.id && !semanticOverviewActive && svgRef.current && zoomBehaviorRef.current) { + d3.select(svgRef.current).call(zoomBehaviorRef.current.transform, d3.zoomTransform(svgRef.current)); + } + }, [semanticOverviewActive, currentGraph?.id]); + // True only while "Reset layout" is re-flowing — suspends snapshot pinning const layoutReflowingRef = useRef(false); const mousedownNodeRef = useRef(null); @@ -5434,18 +5483,35 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i return ( -
+
+ > + + {currentGraph && workItems.length >= 40 && } + +

Force-directed graph of work items and their dependencies. Double-click with a mouse or tap an item to edit it. Use the view switcher to see the same data as a list, table, or board, which are keyboard-navigable.

+ + {/* Empty State Overlay */} {showEmptyStateOverlay && ( diff --git a/packages/web/src/components/SemanticGraphOverview.tsx b/packages/web/src/components/SemanticGraphOverview.tsx new file mode 100644 index 00000000..9e6b8c70 --- /dev/null +++ b/packages/web/src/components/SemanticGraphOverview.tsx @@ -0,0 +1,462 @@ +import { useEffect, useId, useMemo, useRef, useState } from 'react'; +import { + overviewAtScale, + placeOverviewLabels, + projectSemanticGraph, + type SemanticEdge, + type SemanticItem, +} from '../lib/semanticLOD'; + +interface Props { + items: readonly SemanticItem[]; + edges: readonly SemanticEdge[]; + camera: { x: number; y: number; scale: number }; + readPositions: () => readonly { id: string; x: number; y: number }[]; + onActiveChange: (active: boolean) => void; + onInspect: (id: string, beginSession: boolean) => void; + selectedId?: string; + onBrowse: () => void; + onBack: () => void; +} + +/** A display-only companion to the existing D3 camera. Summary labels are placed + * in screen space and linked to source centroids. No projected object is editable + * or persisted. The complete group/member list is the fallback when the screen + * cannot fit every label (especially on phones). */ +export function SemanticGraphOverview({ + items, + edges, + camera, + readPositions, + onActiveChange, + onInspect, + onBack, + selectedId, + onBrowse, +}: Props) { + const projection = useMemo( + () => projectSemanticGraph(items, edges), + [items, edges] + ); + const host = useRef(null); + const [size, setSize] = useState({ width: 0, height: 0 }); + const [mode, setMode] = useState<'auto' | 'overview' | 'items'>('auto'); + const [automatic, setAutomatic] = useState(() => + overviewAtScale(false, camera.scale) + ); + const [groupId, setGroupId] = useState(null); + const [browse, setBrowse] = useState(false); + const [search, setSearch] = useState(''); + const [canReturn, setCanReturn] = useState(false); + const returnMode = useRef('auto'); + const closeButton = useRef(null); + const backButton = useRef(null); + const returnFocus = useRef(null); + const groupListButton = useRef(null); + useEffect(() => { + if (browse) { + onBrowse(); + closeButton.current?.focus(); + } + }, [browse, onBrowse]); + const active = mode === 'overview' || (mode === 'auto' && automatic); + const markerId = `lod-arrow-${useId().replace(/:/g, '')}`; + const group = projection.groups.find(candidate => candidate.id === groupId); + useEffect(() => { + setAutomatic(previous => overviewAtScale(previous, camera.scale)); + }, [camera.scale]); + useEffect(() => { + onActiveChange(active); + return () => onActiveChange(false); + }, [active, onActiveChange]); + useEffect(() => { + if (!host.current) return; + const observer = new ResizeObserver(entries => { + const { width, height } = entries[0].contentRect; + setSize({ width, height }); + }); + observer.observe(host.current); + return () => observer.disconnect(); + }, []); + useEffect(() => { + if (groupId && !group) { + setGroupId(null); + setSearch(''); + } + }, [groupId, group]); + const positions = new Map( + readPositions().map(position => [position.id, position]) + ); + const anchors = projection.groups.map(entry => { + const points = entry.members + .map(item => positions.get(item.id)) + .filter( + point => point && Number.isFinite(point.x) && Number.isFinite(point.y) + ); + const x = + points.reduce((sum, point) => sum + point!.x, 0) / (points.length || 1); + const y = + points.reduce((sum, point) => sum + point!.y, 0) / (points.length || 1); + return { + id: entry.id, + x: x * camera.scale + camera.x, + 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( + () => placeOverviewLabels(anchors, size.width, size.height, obstacles), + // eslint-disable-next-line react-hooks/exhaustive-deps + [projection, size.width, size.height, obstacleKey, active] + ); + const anchorById = new Map(anchors.map(anchor => [anchor.id, anchor])); + const placed = layout.placed.map(entry => ({ + ...entry, + anchorX: anchorById.get(entry.id)!.x, + anchorY: anchorById.get(entry.id)!.y, + })); + const hidden = layout.hidden; + const placedById = new Map(placed.map(entry => [entry.id, entry])); + const groupsById = new Map(projection.groups.map(entry => [entry.id, entry])); + const openGroup = (id: string) => { + if (!browse) returnFocus.current = document.activeElement as HTMLElement; + setGroupId(id); + setBrowse(true); + setSearch(''); + }; + const closeBrowser = () => { + setBrowse(false); + setGroupId(null); + setSearch(''); + (returnFocus.current?.isConnected + ? returnFocus.current + : groupListButton.current + )?.focus(); + }; + const back = () => { + closeBrowser(); + setMode(returnMode.current); + setCanReturn(false); + onBack(); + }; + return ( +
event.stopPropagation()} + > +
+ + + {canReturn && ( + + )} + {active && hidden.length > 0 && ( + + {hidden.length} more in group list + + )} +
+ {active && ( + <> + + {placed.map(entry => { + const summary = groupsById.get(entry.id)!; + return ( + + ); + })} + + )} + {browse && ( +
{ + if (event.key === 'Escape') { + event.stopPropagation(); + closeBrowser(); + } + }} + className="absolute inset-x-3 top-28 bottom-3 sm:top-16 sm:inset-x-auto sm:left-16 sm:w-96 pointer-events-auto overflow-auto rounded-xl border border-gray-600 bg-gray-950 p-4 text-gray-100 shadow-xl" + data-graph-fit-obstacle="true" + > +
+

+ {group?.label ?? 'Graph groups'} +

+ +
+ {group ? ( + <> + +

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

+ +
    + {group.members + .filter(item => + `${item.title} ${item.id}` + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase()) + ) + .map(item => ( +
  • + +
  • + ))} +
+

+ Relationships between groups +

+
    + {projection.bundles + .filter( + bundle => + bundle.source === group.id || bundle.target === group.id + ) + .map(bundle => ( +
  • + +
  • + ))} +
+ + ) : ( + <> +

+ {items.length} loaded items · {edges.length} loaded + relationships + {projection.pendingEdges.length > 0 + ? ` · ${projection.pendingEdges.length} awaiting endpoints` + : ''} +

+
    + {projection.groups.map(entry => ( +
  • + +
  • + ))} +
+ {projection.diagnostics.map(message => ( +

+ {message} +

+ ))} + + )} +
+ )} +
+ ); +} diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 672106e4..afe12a75 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -2,6 +2,15 @@ @tailwind components; @tailwind utilities; +/* Overview summaries own interaction at far zoom. Keep the D3 world/camera alive + underneath, but hidden source hit targets must not edit or intercept clicks. */ +.graph-container[data-overview="true"] > svg .nodes-group, +.graph-container[data-overview="true"] > svg .edges-group, +.graph-container[data-overview="true"] > svg .arrows-group, +.graph-container[data-overview="true"] > svg .edge-labels-group { + display: none; +} + /* Lagoon Animation Parameters - Easy to edit and randomize */ :root { /* Layer 1 Parameters */ diff --git a/packages/web/src/lib/semanticLOD.test.ts b/packages/web/src/lib/semanticLOD.test.ts new file mode 100644 index 00000000..7dc57662 --- /dev/null +++ b/packages/web/src/lib/semanticLOD.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; +import { + metadataRecord, + overviewAtScale, + placeOverviewLabels, + projectSemanticGraph, + type SemanticItem, +} from './semanticLOD'; + +const item = (id: string, metadata?: unknown): SemanticItem => ({ + id, + title: `Item ${id}`, + type: 'TASK', + status: 'PROPOSED', + metadata, +}); +const edge = ( + id: string, + source: string, + target: string, + type = 'DEPENDS_ON' +) => ({ id, source, target, type }); + +describe('semantic graph projection', () => { + it('conserves source identity and every directed typed relationship without mutating inputs', () => { + const items = [ + item('a', { lane: 'one' }), + item('b', { lane: 'one' }), + item('c', { lane: 'two' }), + ]; + const edges = [ + edge('internal', 'a', 'b'), + edge('loop', 'a', 'a'), + edge('parallel1', 'a', 'c'), + edge('parallel2', 'b', 'c'), + edge('reverse', 'c', 'a'), + edge('block', 'a', 'c', 'BLOCKS'), + edge('missing', 'a', 'unloaded'), + ]; + const before = JSON.stringify({ items, edges }); + const projected = projectSemanticGraph(items, edges); + expect(projected.groups.flatMap(group => group.members)).toEqual( + expect.arrayContaining(items) + ); + expect(projected.groups.flatMap(group => group.members)).toHaveLength( + items.length + ); + expect(projected.groups[0].members[0]).toBe(items[0]); + const accounted = [ + ...projected.groups.flatMap(group => group.internalEdges), + ...projected.bundles.flatMap(bundle => bundle.edges), + ...projected.pendingEdges, + ]; + expect(accounted.map(value => value.id).sort()).toEqual( + edges.map(value => value.id).sort() + ); + expect(projected.bundles).toHaveLength(3); + expect(projected.bundles.map(bundle => bundle.edges.length).sort()).toEqual( + [1, 1, 2] + ); + expect(JSON.stringify({ items, edges })).toBe(before); + }); + + it('uses explicit paths ahead of native containment and never treats dependencies as parents', () => { + const items = [ + item('parent'), + item('child'), + item( + 'explicit', + JSON.stringify({ lod: { groupPath: ['Subsystem', 'Worker'] } }) + ), + item('dependency', { lane: 'loose' }), + ]; + const projection = projectSemanticGraph(items, [ + edge('1', 'child', 'parent', 'IS_PART_OF'), + edge('2', 'parent', 'explicit', 'CONTAINS'), + edge('3', 'dependency', 'child'), + ]); + expect(projection.membership.get('parent')).toBe( + projection.membership.get('child') + ); + expect(projection.membership.get('explicit')).not.toBe( + projection.membership.get('parent') + ); + expect( + projection.groups.find(group => + group.members.some(member => member.id === 'explicit') + )?.label + ).toBe('Subsystem'); + expect(projection.membership.get('dependency')).not.toBe( + projection.membership.get('parent') + ); + expect(projection.diagnostics).toHaveLength(1); + }); + + it('terminates cycles and chooses stable membership through input reordering and multiple parents', () => { + const items = ['a', 'b', 'c', 'd'].map(id => item(id)); + const edges = [ + edge('1', 'a', 'b', 'IS_PART_OF'), + edge('2', 'b', 'a', 'IS_PART_OF'), + edge('3', 'c', 'b', 'IS_PART_OF'), + edge('4', 'd', 'c', 'IS_PART_OF'), + edge('5', 'd', 'a', 'IS_PART_OF'), + ]; + const first = projectSemanticGraph(items, edges); + const second = projectSemanticGraph( + [...items].reverse(), + [...edges].reverse() + ); + expect([...first.membership]).toEqual([...second.membership]); + expect(first.groups).toHaveLength(1); + expect(first.groups[0].members).toHaveLength(4); + expect(first.diagnostics).toHaveLength(2); + expect(first.groups[0].internalEdges).toHaveLength(5); + }); + + it('recovers malformed metadata and keeps label collisions in separate namespaces', () => { + for (const metadata of ['{bad', 'null', '[]', null, 3]) + expect(metadataRecord(metadata)).toEqual({}); + const projection = projectSemanticGraph( + [ + item('a', { lane: 'TASK' }), + item('b', { kind: 'TASK' }), + item('c', '{bad'), + ], + [] + ); + expect(projection.groups).toHaveLength(3); + expect(new Set(projection.groups.map(group => group.id)).size).toBe(3); + }); + + it('retains group identity and canonical positions through unrelated snapshot updates', () => { + const a = { + ...item('a', { groupPath: 'System/Worker' }), + positionX: 120, + positionY: 200, + }; + const before = projectSemanticGraph([a], []); + const after = projectSemanticGraph( + [item('new', { groupPath: 'System/API' }), a], + [] + ); + expect(after.membership.get(a.id)).toBe(before.membership.get(a.id)); + expect(after.groups[0].members.find(member => member.id === a.id)).toBe(a); + expect(a.positionX).toBe(120); + expect(a.positionY).toBe(200); + }); +}); + +describe('readable overview', () => { + it('retains parent coverage across the zoom dead zone and resists boundary jitter', () => { + let visible = false; + const states = [0.1, 0.84, 0.86, 0.84, 0.99, 1.01, 0.99, 0.86, 0.84].map( + scale => (visible = overviewAtScale(visible, scale)) + ); + expect(states).toEqual([ + true, + true, + true, + true, + true, + false, + false, + false, + true, + ]); + }); + it.each([ + [390, 600], + [1280, 800], + [320, 180], + ])( + 'has no label collisions at %i × %i and accounts for overflow', + (width, height) => { + const anchors = Array.from({ length: 40 }, (_, index) => ({ + id: String(index), + x: width / 2, + y: height / 2, + })); + const layout = placeOverviewLabels(anchors, width, height); + expect(layout.placed.length + layout.hidden.length).toBe(anchors.length); + for (const a of layout.placed) { + expect(a.x).toBeGreaterThanOrEqual(0); + expect(a.x + a.width).toBeLessThanOrEqual(width); + expect(a.y + a.height).toBeLessThanOrEqual(height); + for (const b of layout.placed.filter(b => b.id !== a.id)) { + expect( + a.x < b.x + b.width && + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y + ).toBe(false); + } + } + } + ); +}); diff --git a/packages/web/src/lib/semanticLOD.ts b/packages/web/src/lib/semanticLOD.ts new file mode 100644 index 00000000..416989f3 --- /dev/null +++ b/packages/web/src/lib/semanticLOD.ts @@ -0,0 +1,276 @@ +/** GraphDone's display projection. These objects must never enter the simulation + * or position-save queue. Source IDs and typed, directed edges remain canonical. */ +export interface SemanticItem { + id: string; + title: string; + type: string; + status: string; + metadata?: unknown; +} +export interface SemanticEdge { + id: string; + source: string | { id: string }; + target: string | { id: string }; + type: string; +} +export interface SemanticGroup { + id: string; + label: string; + basis: 'path' | 'containment' | 'lane' | 'kind' | 'type'; + members: SemanticItem[]; + internalEdges: SemanticEdge[]; + blocked: number; + completed: number; +} +export interface SemanticBundle { + id: string; + source: string; + target: string; + type: string; + edges: SemanticEdge[]; +} + +const endpoint = (value: SemanticEdge['source']) => + typeof value === 'string' ? value : value.id; +const text = (value: unknown): string | undefined => + typeof value === 'string' && value.trim() ? value.trim() : undefined; +const label = (value: string) => value.replace(/[_-]+/g, ' '); + +export function metadataRecord(value: unknown): Record { + try { + const parsed = typeof value === 'string' ? JSON.parse(value) : value; + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? parsed + : {}; + } catch { + return {}; + } +} + +/** First slice: top-level paths, native containment, release lanes, then kinds / + * types. Never interpret dependency edges as containment. Multiple parents use + * a deterministic primary parent; all original relationships stay accounted for. */ +export function projectSemanticGraph( + items: readonly SemanticItem[], + edges: readonly SemanticEdge[] +) { + const byId = new Map(items.map(item => [item.id, item])); + const parents = new Map>(); + const diagnostics = new Set(); + for (const edge of edges) { + const source = endpoint(edge.source), + target = endpoint(edge.target); + const child = + edge.type === 'IS_PART_OF' + ? source + : edge.type === 'CONTAINS' + ? target + : undefined; + const parent = edge.type === 'IS_PART_OF' ? target : source; + if (child && byId.has(child) && byId.has(parent)) { + if (!parents.has(child)) parents.set(child, new Set()); + parents.get(child)!.add(parent); + } + } + for (const candidates of parents.values()) { + if (candidates.size > 1) + diagnostics.add( + 'Some items have multiple containers; the lowest ID is used for the overview.' + ); + } + const roots = new Map(); + const rootFor = (id: string): string => { + if (roots.has(id)) return roots.get(id)!; + const path: string[] = []; + const visited = new Map(); + let next = id; + while (!roots.has(next) && !visited.has(next)) { + visited.set(next, path.length); + path.push(next); + const parent = [...(parents.get(next) ?? [])].sort()[0]; + if (!parent) break; + next = parent; + } + let root = roots.get(next) ?? next; + if (parents.has(next) && visited.has(next)) { + root = path.slice(visited.get(next)).sort()[0]; + diagnostics.add( + 'Containment cycles are grouped together; original relationships are retained.' + ); + } + for (const member of path) roots.set(member, root); + return root; + }; + const containerIds = new Set( + [...parents.values()].flatMap(values => [...values]) + ); + const groups = new Map(); + const membership = new Map(); + for (const item of [...byId.values()].sort((a, b) => + a.id.localeCompare(b.id) + )) { + const metadata = metadataRecord(item.metadata); + const lod = metadataRecord(metadata.lod); + const rawPath = lod.groupPath ?? metadata.groupPath; + const path = Array.isArray(rawPath) + ? text(rawPath[0]) + : text(rawPath) + ?.split('/') + .map(p => p.trim()) + .find(Boolean); + const root = rootFor(item.id); + let basis: SemanticGroup['basis'], key: string, title: string; + if (path) { + basis = 'path'; + key = path; + title = path; + if (parents.has(item.id)) + diagnostics.add( + 'Explicit group paths take precedence over containment in this overview.' + ); + } else if (parents.has(item.id) || containerIds.has(item.id)) { + basis = 'containment'; + key = root; + title = byId.get(root)!.title; + } else if (text(metadata.lane)) { + basis = 'lane'; + key = text(metadata.lane)!; + title = label(key); + } else if (text(metadata.kind)) { + basis = 'kind'; + key = text(metadata.kind)!; + title = label(key); + } else { + basis = 'type'; + key = item.type || 'Items'; + title = label(key); + } + const id = JSON.stringify(['group', basis, key]); + if (!groups.has(id)) + groups.set(id, { + id, + label: title, + basis, + members: [], + internalEdges: [], + blocked: 0, + completed: 0, + }); + const group = groups.get(id)!; + group.members.push(item); + if (item.status === 'BLOCKED') group.blocked++; + if (item.status === 'COMPLETED') group.completed++; + membership.set(item.id, id); + } + const bundles = new Map(); + const pendingEdges: SemanticEdge[] = []; + for (const edge of edges) { + const source = membership.get(endpoint(edge.source)), + target = membership.get(endpoint(edge.target)); + if (!source || !target) { + pendingEdges.push(edge); + continue; + } + if (source === target) { + groups.get(source)!.internalEdges.push(edge); + continue; + } + const id = JSON.stringify([source, target, edge.type]); + if (!bundles.has(id)) + bundles.set(id, { id, source, target, type: edge.type, edges: [] }); + bundles.get(id)!.edges.push(edge); + } + return { + groups: [...groups.values()], + bundles: [...bundles.values()], + membership, + pendingEdges, + diagnostics: [...diagnostics], + }; +} + +/** Hysteresis keeps the parent readable throughout the transition band. Inspired + * by CodeGraph's parent fallback; independent GraphDone policy/implementation. */ +export function overviewAtScale(previous: boolean, scale: number): boolean { + // Long, multiline source titles are fitted inside cards; the nominal 12px + // counter-scale alone cannot keep them readable below normal item scale. + return previous ? scale < 1 : scale < 0.85; +} + +export interface OverviewAnchor { + id: string; + x: number; + y: number; +} +/** Screen-space placement keeps labels readable without changing source layout. + * A bounded grid offers non-overlapping slots; nearest slots retain spatial + * context. Excess groups stay reachable in the overview's complete group list. */ +export function placeOverviewLabels( + anchors: readonly OverviewAnchor[], + width: number, + height: number, + obstacles: readonly { + left: number; + top: number; + right: number; + bottom: number; + }[] = [] +) { + const cardWidth = Math.min(184, Math.max(120, width - 32)), + cardHeight = 72; + const columns = Math.max(1, Math.floor((width - 32) / (cardWidth + 16))); + const rows = Math.max(0, Math.floor((height - 100) / (cardHeight + 24))); + const slots: { x: number; y: number }[] = []; + const span = columns * (cardWidth + 16) - 16; + for (let row = 0; row < rows; row++) + for (let col = 0; col < columns; col++) { + const slot = { + x: (width - span) / 2 + col * (cardWidth + 16), + y: 64 + row * (cardHeight + 24), + }; + if ( + !obstacles.some( + obstacle => + slot.x < obstacle.right + 8 && + slot.x + cardWidth > obstacle.left - 8 && + slot.y < obstacle.bottom + 8 && + slot.y + cardHeight > obstacle.top - 8 + ) + ) + slots.push(slot); + } + const placed: (OverviewAnchor & { + anchorX: number; + anchorY: number; + width: number; + height: number; + })[] = []; + const hidden: string[] = []; + for (const anchor of [...anchors].sort((a, b) => a.id.localeCompare(b.id))) { + if (!slots.length) { + hidden.push(anchor.id); + continue; + } + let best = 0, + distance = Infinity; + slots.forEach((slot, index) => { + const d = + (slot.x + cardWidth / 2 - anchor.x) ** 2 + + (slot.y + cardHeight / 2 - anchor.y) ** 2; + if (d < distance) { + distance = d; + best = index; + } + }); + const slot = slots.splice(best, 1)[0]; + placed.push({ + ...anchor, + ...slot, + anchorX: anchor.x, + anchorY: anchor.y, + width: cardWidth, + height: cardHeight, + }); + } + return { placed, hidden }; +} diff --git a/packages/web/src/types/graph.ts b/packages/web/src/types/graph.ts index f4f3c82f..5d707c39 100644 --- a/packages/web/src/types/graph.ts +++ b/packages/web/src/types/graph.ts @@ -80,6 +80,7 @@ export interface WorkItem { priority?: number; dueDate?: string; tags?: string[]; + metadata?: string | Record | null; assignedTo?: { id: string; name: string; @@ -175,4 +176,4 @@ export interface GraphContextType { canDeleteGraph: (graphId: string) => boolean; canShareGraph: (graphId: string) => boolean; refreshGraphs: () => Promise; -} \ No newline at end of file +} diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts new file mode 100644 index 00000000..7c9f6745 --- /dev/null +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -0,0 +1,439 @@ +import { test, expect } from '@playwright/test'; +import { login, TEST_USERS } from '../../lib/auth'; +import { gql } from '../../lib/graphRequest'; +import { seedLargeGraph, deleteGraphDeep } from '../../lib/seedGraph'; + +test.use({ actionTimeout: 10000 }); + +for (const width of [1440, 390]) + test(`semantic overview preserves 301 items and layout at ${width}px @lod`, async ({ + page, + }, info) => { + test.setTimeout(120000); + await page.setViewportSize({ width, height: 900 }); + const touch = + width === 390 ? await page.context().newCDPSession(page) : null; + if (touch) + await touch.send('Emulation.setTouchEmulationEnabled', { + enabled: true, + maxTouchPoints: 2, + }); + await login(page, TEST_USERS.ADMIN); + const seeded = await seedLargeGraph(page, { + size: 301, + spacing: 300, + edgeFactor: 370 / 301, + namePrefix: 'Semantic LOD', + batchSize: 20, + titleForIndex: (index, type) => + index === 8 + ? `${type} 8 — Follow a request through gateway validation, event delivery, retries, settlement and reconciliation across the entire system` + : `${type} ${index}`, + metadataForIndex: index => ({ + lod: { groupPath: [`Subsystem ${index % 8}`, 'Worker'] }, + }), + }); + const attemptedWrites: string[] = []; + let otherGraph: Awaited> | undefined; + const pageErrors: string[] = []; + page.on('pageerror', error => pageErrors.push(error.message)); + const snapshot = async () => + ( + await gql( + page, + `query($id:ID!){workItems(where:{graph:{id:$id}}){id positionX positionY}}`, + { id: seeded.graphId } + ) + ).workItems.sort((a: { id: string }, b: { id: string }) => + a.id.localeCompare(b.id) + ); + try { + if (width === 1440) + otherGraph = await seedLargeGraph(page, { + size: 40, + spacing: 300, + batchSize: 20, + namePrefix: 'LOD switch target', + }); + const graphNames = (await gql(page, 'query{graphs{id name}}')).graphs as { + id: string; + name: string; + }[]; + const switchGraph = async (id: string) => { + await page + .getByTestId( + width === 390 ? 'graph-selector-mobile' : 'graph-selector' + ) + .click(); + await page + .getByTestId('graph-selector-dropdown') + .getByText(graphNames.find(graph => graph.id === id)!.name, { + exact: true, + }) + .click(); + // This selector intentionally stays open after changing graphs. + await page.keyboard.press('Escape'); + await expect(page.getByTestId('graph-selector-dropdown')).toHaveCount( + 0 + ); + }; + // (0,0) means unplaced in the existing renderer. This fixture uses only + // deliberately pinned positions so navigation must perform zero writes. + await gql( + page, + `mutation($id:ID!){updateWorkItems(where:{graph:{id:$id},positionX:0,positionY:0},update:{positionX:1}){workItems{id}}}`, + { id: seeded.graphId } + ); + const before = await snapshot(); + await page.route('**/api/graphql', async route => { + const body = route.request().postDataJSON(); + if (/^\s*mutation\b/.test(body?.query ?? '')) { + attemptedWrites.push(body.operationName ?? 'unnamed mutation'); + await route.abort('blockedbyclient'); + } else await route.continue(); + }); + await page.evaluate(id => { + localStorage.setItem('currentGraphId', id); + localStorage.setItem('graphdone:viewMode', 'graph'); + }, seeded.graphId); + await page.goto('/'); + await expect(page.locator('.graph-container .node')).toHaveCount(301, { + timeout: 30000, + }); + const overview = page.locator('.semantic-overview'); + await expect(overview).toHaveAttribute('data-active', 'true'); + const cards = page.locator('[data-lod-group]'); + const svg = page.locator('.graph-container > svg'); + await svg.evaluate(element => { + element.setAttribute('data-lod-identity', 'retained'); + }); + expect(await cards.count()).toBeGreaterThan(0); + const geometry = await cards.evaluateAll(elements => + elements.map(element => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + right: rect.right, + bottom: rect.bottom, + font: parseFloat( + getComputedStyle(element.querySelector('span')!).fontSize + ), + }; + }) + ); + for (const a of geometry) { + expect(a.font).toBeGreaterThanOrEqual(14); + expect(a.x).toBeGreaterThanOrEqual(0); + expect(a.right).toBeLessThanOrEqual(width); + for (const b of geometry.filter(b => b !== a)) + expect( + a.x < b.right && a.right > b.x && a.y < b.bottom && a.bottom > b.y + ).toBe(false); + } + await info.attach('overview-geometry.json', { + body: JSON.stringify({ + width, + nodes: before.length, + edges: seeded.edgeCount, + geometry, + }), + contentType: 'application/json', + }); + await page.screenshot({ + path: `test-artifacts/quality/semantic-lod-${width}.png`, + }); + const longTitleItem = before.find( + (item: { id: string }) => item.id === seeded.nodeIds[8] + ); + const zoomToTitle = async (scale: number) => { + await page.evaluate( + ({ x, y, scale }) => + ( + window as unknown as { + miniMapZoom: (x: number, y: number, k: number) => void; + } + ).miniMapZoom(x, y, scale), + { x: longTitleItem.positionX, y: longTitleItem.positionY, scale } + ); + await expect + .poll(() => + svg.evaluate( + element => + (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k + ) + ) + .toBeCloseTo(scale); + }; + for (const scale of [0.5, 0.9, 0.99]) { + await zoomToTitle(scale); + await expect(overview).toHaveAttribute('data-active', 'true'); + } + await zoomToTitle(1.05); + await expect(overview).toHaveAttribute('data-active', 'false'); + const titleSizes = await svg.locator('.node-title-text').evaluateAll( + (elements, id) => + elements + .filter( + element => + ( + element.closest('.node') as Element & { + __data__?: { id: string }; + } + )?.__data__?.id === id + ) + .map(element => { + const matrix = (element as SVGGraphicsElement).getScreenCTM()!; + return ( + parseFloat(getComputedStyle(element).fontSize) * + Math.hypot(matrix.a, matrix.b) + ); + }), + seeded.nodeIds[8] + ); + expect(titleSizes.length).toBeGreaterThan(0); + expect(Math.min(...titleSizes)).toBeGreaterThanOrEqual(12); + await zoomToTitle(0.23); + await expect(overview).toHaveAttribute('data-active', 'true'); + await overview + .getByRole('button', { name: '8 groups · 301 items' }) + .click(); + await expect( + page + .getByRole('region', { name: 'Graph groups' }) + .getByRole('button', { name: /Subsystem/ }) + ).toHaveCount(8); + await page + .getByRole('region', { name: 'Graph groups' }) + .getByRole('button', { name: /Subsystem 0/ }) + .click(); + const members = page.getByRole('region', { name: 'Subsystem 0 members' }); + await expect( + members.getByRole('button', { + name: /TASK|BUG|FEATURE|MILESTONE|OUTCOME/, + }) + ).toHaveCount(38); + await members + .getByRole('textbox', { name: 'Find an item' }) + .fill('TASK 0'); + await members + .getByRole('button', { name: 'TASK 0 TASK · PROPOSED', exact: true }) + .click(); + await expect(page.getByTestId('node-inspector')).toBeVisible(); + await expect(page.getByTestId('node-inspector')).toContainText('TASK 0'); + await expect(overview).toHaveAttribute('data-active', 'false'); + await expect(page.locator('.graph-container')).not.toHaveAttribute( + 'data-overview', + 'true' + ); + await expect( + overview.getByRole('button', { name: 'Back to overview' }) + ).toBeVisible(); + await expect( + overview.getByRole('button', { name: 'Back to overview' }) + ).toBeFocused(); + await overview.getByRole('button', { name: 'Back to overview' }).click(); + await expect(overview).toHaveAttribute('data-active', 'true'); + await expect(page.getByTestId('node-inspector')).toHaveCount(0); + // Real wheel input over a summary must use the retained D3 camera. + const initialScale = await svg.evaluate( + element => + (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k + ); + await cards.first().hover(); + await page.mouse.wheel(0, -180); + await expect + .poll(() => + svg.evaluate( + element => + (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k + ) + ) + .toBeGreaterThan(initialScale); + const transform = () => + svg.evaluate(element => { + const { x, y, k } = ( + element as SVGSVGElement & { + __zoom: { x: number; y: number; k: number }; + } + ).__zoom; + return { x, y, k }; + }); + const beforePan = await transform(); + const cardBounds = (await cards.first().boundingBox())!; + const x = cardBounds.x + cardBounds.width / 2, + y = cardBounds.y + cardBounds.height / 2; + if (touch) { + await touch.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [{ x, y, id: 1 }], + }); + await touch.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [{ x: x + 35, y: y + 15, id: 1 }], + }); + await touch.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + }); + await expect + .poll(async () => (await transform()).x) + .not.toBe(beforePan.x); + const beforePinch = await transform(); + await touch.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [ + { x: x - 20, y, id: 1 }, + { x: x + 20, y, id: 2 }, + ], + }); + await touch.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [ + { x: x - 40, y, id: 1 }, + { x: x + 40, y, id: 2 }, + ], + }); + await touch.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + }); + await expect + .poll(async () => (await transform()).k) + .toBeGreaterThan(beforePinch.k); + } else { + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + 35, y + 15, { steps: 5 }); + await page.mouse.up(); + await expect + .poll(async () => (await transform()).x) + .not.toBe(beforePan.x); + } + await expect(page.getByRole('region', { name: /members/ })).toHaveCount( + 0 + ); + // Keyboard opening/escape returns focus to the summary, including touch devices. + await cards.first().focus(); + await page.keyboard.press('Enter'); + await expect( + page.getByRole('button', { name: 'Close group browser' }) + ).toBeFocused(); + await page.keyboard.press('Escape'); + await expect(cards.first()).toBeFocused(); + await page.keyboard.press('Enter'); + const directMembers = page.getByRole('region', { name: /members/ }); + await directMembers + .locator('ul') + .first() + .getByRole('button') + .first() + .focus(); + await page.keyboard.press('Enter'); + await expect(page.getByTestId('node-inspector')).toBeVisible(); + await expect( + overview.getByRole('button', { name: 'Back to overview' }) + ).toBeFocused(); + await page.keyboard.press('Enter'); + await expect( + overview.getByRole('button', { name: '8 groups · 301 items' }) + ).toBeFocused(); + + await page.evaluate(() => + ( + window as unknown as { + miniMapZoom: (x: number, y: number, k: number) => void; + } + ).miniMapZoom(0, 0, 1.2) + ); + await expect.poll(async () => (await transform()).k).toBeCloseTo(1.2); + await overview + .getByRole('combobox', { name: 'Graph detail' }) + .selectOption('overview'); + await expect( + page.locator('.graph-container > svg .nodes-group') + ).toBeHidden(); + await expect( + page.locator('.graph-container > svg .arrows-group') + ).toBeHidden(); + expect(await svg.locator(':scope > defs #node-drop-shadow').count()).toBe( + 1 + ); + expect(await overview.locator('defs filter').count()).toBe(0); + const inspectFirst = async () => { + await overview + .getByRole('button', { name: '8 groups · 301 items' }) + .click(); + await page + .getByRole('region', { name: 'Graph groups' }) + .getByRole('button', { name: /Subsystem 0/ }) + .click(); + const region = page.getByRole('region', { + name: 'Subsystem 0 members', + }); + await region + .getByRole('textbox', { name: 'Find an item' }) + .fill('TASK 0'); + await region + .getByRole('button', { name: 'TASK 0 TASK · PROPOSED', exact: true }) + .focus(); + await page.keyboard.press('Enter'); + await expect(page.getByTestId('node-inspector')).toContainText( + 'TASK 0' + ); + await expect( + overview.getByRole('button', { name: 'Back to overview' }) + ).toBeFocused(); + }; + await inspectFirst(); + await page.keyboard.press('Enter'); + await expect( + overview.getByRole('combobox', { name: 'Graph detail' }) + ).toHaveValue('overview'); + await expect(overview).toHaveAttribute('data-active', 'true'); + // Representation changes retain the SVG within a graph scope. The existing + // app may replace it when changing to a different graph. + await expect(svg).toHaveAttribute('data-lod-identity', 'retained'); + if (otherGraph) { + await inspectFirst(); // leave a saved A camera before switching A → B → A + await switchGraph(otherGraph.graphId); + await expect(page.locator('.graph-container .node')).toHaveCount(40); + await switchGraph(seeded.graphId); + await expect(page.locator('.graph-container .node')).toHaveCount(301); + await page.evaluate(() => + ( + window as unknown as { + miniMapZoom: (x: number, y: number, k: number) => void; + } + ).miniMapZoom(0, 0, 0.23) + ); + await expect.poll(async () => (await transform()).k).toBeCloseTo(0.23); + const returnCamera = await transform(); + await svg.evaluate(element => + element.setAttribute('data-lod-identity', 'returned-graph') + ); + await inspectFirst(); + await page.keyboard.press('Enter'); + await expect.poll(transform).toEqual(returnCamera); + await expect(svg).toHaveAttribute( + 'data-lod-identity', + 'returned-graph' + ); + } + await page.waitForTimeout(1000); // let debounced persistence attempts surface + expect(await snapshot()).toEqual(before); + expect(attemptedWrites).toEqual([]); + expect(pageErrors).toEqual([]); + await page.reload(); + await expect(page.locator('.graph-container .node')).toHaveCount(301); + expect(await snapshot()).toEqual(before); + expect(attemptedWrites).toEqual([]); + } finally { + if (!page.isClosed()) { + await page.unroute('**/api/graphql'); + await deleteGraphDeep(page, seeded.graphId); + if (otherGraph) await deleteGraphDeep(page, otherGraph.graphId); + } + await touch?.detach(); + } + }); diff --git a/tests/lib/seedGraph.ts b/tests/lib/seedGraph.ts index b8d267d2..5e611c60 100644 --- a/tests/lib/seedGraph.ts +++ b/tests/lib/seedGraph.ts @@ -55,6 +55,10 @@ export interface SeedOptions { /** grid spacing in px (default 130). */ spacing?: number; namePrefix?: string; + metadataForIndex?: (index: number) => Record; + titleForIndex?: (index: number, type: string) => string; + /** Smaller repeated mutation shapes avoid Neo4j planning hundreds of creates. */ + batchSize?: number; } export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise { @@ -81,19 +85,20 @@ export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise Date: Tue, 27 Oct 2026 09:13:44 -0700 Subject: [PATCH 04/14] fix: retain readable detail through both zoom directions --- docs/architecture/dynamic-graph-lod.md | 5 ++- packages/web/src/lib/semanticLOD.test.ts | 2 +- packages/web/src/lib/semanticLOD.ts | 2 +- tests/e2e/graph/semantic-lod.spec.ts | 54 +++++++++++++----------- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md index 40537dab..2f3a9bfd 100644 --- a/docs/architecture/dynamic-graph-lod.md +++ b/docs/architecture/dynamic-graph-lod.md @@ -1,7 +1,8 @@ # Dynamic graph LOD: continuation through delivery -Reviewed 2026-10-27 UTC. Status: implementation plan, with fresh source, GitHub, -and localhost evidence. This document does not claim semantic LOD is implemented. +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. The stage IDs below are proposed work items, not existing GitHub issue numbers. The owner's CodeGraph reuse direction is incorporated in the diff --git a/packages/web/src/lib/semanticLOD.test.ts b/packages/web/src/lib/semanticLOD.test.ts index 7dc57662..dce9763a 100644 --- a/packages/web/src/lib/semanticLOD.test.ts +++ b/packages/web/src/lib/semanticLOD.test.ts @@ -150,7 +150,7 @@ describe('semantic graph projection', () => { describe('readable overview', () => { it('retains parent coverage across the zoom dead zone and resists boundary jitter', () => { let visible = false; - const states = [0.1, 0.84, 0.86, 0.84, 0.99, 1.01, 0.99, 0.86, 0.84].map( + const states = [0.1, 0.99, 1.01, 0.99, 1.14, 1.16, 1.14, 1.01, 0.99].map( scale => (visible = overviewAtScale(visible, scale)) ); expect(states).toEqual([ diff --git a/packages/web/src/lib/semanticLOD.ts b/packages/web/src/lib/semanticLOD.ts index 416989f3..bb00afcd 100644 --- a/packages/web/src/lib/semanticLOD.ts +++ b/packages/web/src/lib/semanticLOD.ts @@ -194,7 +194,7 @@ export function projectSemanticGraph( export function overviewAtScale(previous: boolean, scale: number): boolean { // Long, multiline source titles are fitted inside cards; the nominal 12px // counter-scale alone cannot keep them readable below normal item scale. - return previous ? scale < 1 : scale < 0.85; + return previous ? scale < 1.15 : scale < 1; } export interface OverviewAnchor { diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index 7c9f6745..862555c3 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -165,34 +165,38 @@ for (const width of [1440, 390]) ) .toBeCloseTo(scale); }; - for (const scale of [0.5, 0.9, 0.99]) { + for (const scale of [0.5, 0.9, 0.99, 1.1]) { await zoomToTitle(scale); await expect(overview).toHaveAttribute('data-active', 'true'); } - await zoomToTitle(1.05); - await expect(overview).toHaveAttribute('data-active', 'false'); - const titleSizes = await svg.locator('.node-title-text').evaluateAll( - (elements, id) => - elements - .filter( - element => - ( - element.closest('.node') as Element & { - __data__?: { id: string }; - } - )?.__data__?.id === id - ) - .map(element => { - const matrix = (element as SVGGraphicsElement).getScreenCTM()!; - return ( - parseFloat(getComputedStyle(element).fontSize) * - Math.hypot(matrix.a, matrix.b) - ); - }), - seeded.nodeIds[8] - ); - expect(titleSizes.length).toBeGreaterThan(0); - expect(Math.min(...titleSizes)).toBeGreaterThanOrEqual(12); + for (const scale of [1.2, 1.14, 1.01]) { + await zoomToTitle(scale); + await expect(overview).toHaveAttribute('data-active', 'false'); + const titleSizes = await svg.locator('.node-title-text').evaluateAll( + (elements, id) => + elements + .filter( + element => + ( + element.closest('.node') as Element & { + __data__?: { id: string }; + } + )?.__data__?.id === id + ) + .map(element => { + const matrix = (element as SVGGraphicsElement).getScreenCTM()!; + return ( + parseFloat(getComputedStyle(element).fontSize) * + Math.hypot(matrix.a, matrix.b) + ); + }), + seeded.nodeIds[8] + ); + expect(titleSizes.length).toBeGreaterThan(0); + expect(Math.min(...titleSizes)).toBeGreaterThanOrEqual(12); + } + await zoomToTitle(0.99); + await expect(overview).toHaveAttribute('data-active', 'true'); await zoomToTitle(0.23); await expect(overview).toHaveAttribute('data-active', 'true'); await overview From 5f11f3bcc97fa3e90ffa1ea31f8e75f97a1ace16 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 09:19:25 -0700 Subject: [PATCH 05/14] test: require semantic overview in headed acceptance --- package.json | 3 ++- packages/web/src/components/SemanticGraphOverview.tsx | 1 - tests/lib/runner/unified-cli.test.mjs | 1 + tests/sequences/unified.config.mjs | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 10ae27d6..b176a83b 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,14 @@ "report:vlm": "node tests/lib/reporting/generate-vlm-report.mjs", "perf:bundle": "node tests/perf/check-bundle-size.mjs", "qc:modes": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/new-user-modes.spec.ts", - "qc:new-user": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/new-user-modes.spec.ts tests/e2e/quality/button-content.spec.ts tests/e2e/graph/guest-dialog-layout.spec.ts tests/e2e/graph/responsive-real-workflows.spec.ts tests/e2e/graph/overview-board-actions.spec.ts tests/e2e/graph/recorded-ai-work.spec.ts tests/e2e/graph/admin-dialog-responsive.spec.ts tests/e2e/smoke/user-smoke.spec.ts tests/e2e/quality/persona-lifecycle.spec.ts tests/e2e/quality/graph-motion.spec.ts tests/e2e/quality/graph-resize.spec.ts tests/e2e/quality/graph-details.spec.ts", + "qc:new-user": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/new-user-modes.spec.ts tests/e2e/quality/button-content.spec.ts tests/e2e/graph/guest-dialog-layout.spec.ts tests/e2e/graph/responsive-real-workflows.spec.ts tests/e2e/graph/overview-board-actions.spec.ts tests/e2e/graph/recorded-ai-work.spec.ts tests/e2e/graph/admin-dialog-responsive.spec.ts tests/e2e/graph/semantic-lod.spec.ts tests/e2e/smoke/user-smoke.spec.ts tests/e2e/quality/persona-lifecycle.spec.ts tests/e2e/quality/graph-motion.spec.ts tests/e2e/quality/graph-resize.spec.ts tests/e2e/quality/graph-details.spec.ts", "qc:personas": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/persona-lifecycle.spec.ts", "qc:motion": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/graph-motion.spec.ts tests/e2e/quality/graph-resize.spec.ts", "qc:graph-first": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/graph-first-journey.spec.ts", "qc:visual": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/visual-style.spec.ts", "qc:buttons": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/button-content.spec.ts", "qc:controls": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/graph-controls.spec.ts", + "qc:lod": "playwright test tests/e2e/graph/semantic-lod.spec.ts --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --retries=0 --reporter=line", "qc:layout": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/graph-layout.spec.ts", "qc:verification": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/turnstile-guest.spec.ts", "qc:graph": "playwright test --project=GraphDone-Core/dev-neo4j/chromium --headed --workers=1 --reporter=line tests/e2e/quality/graph-motion.spec.ts tests/e2e/quality/graph-resize.spec.ts tests/e2e/quality/graph-details.spec.ts tests/e2e/quality/graph-controls.spec.ts tests/e2e/smoke/user-smoke.spec.ts", diff --git a/packages/web/src/components/SemanticGraphOverview.tsx b/packages/web/src/components/SemanticGraphOverview.tsx index 9e6b8c70..9c0e0d0e 100644 --- a/packages/web/src/components/SemanticGraphOverview.tsx +++ b/packages/web/src/components/SemanticGraphOverview.tsx @@ -125,7 +125,6 @@ export function SemanticGraphOverview({ // anchors; only changed membership/viewport/controls triggers label placement. const layout = useMemo( () => placeOverviewLabels(anchors, size.width, size.height, obstacles), - // eslint-disable-next-line react-hooks/exhaustive-deps [projection, size.width, size.height, obstacleKey, active] ); const anchorById = new Map(anchors.map(anchor => [anchor.id, anchor])); diff --git a/tests/lib/runner/unified-cli.test.mjs b/tests/lib/runner/unified-cli.test.mjs index c9b4eb8b..36d2bf23 100644 --- a/tests/lib/runner/unified-cli.test.mjs +++ b/tests/lib/runner/unified-cli.test.mjs @@ -47,6 +47,7 @@ for (const sequence of ['new-user', 'mobile', 'perf-budgets']) { const result = run(['--sequence', sequence]); assert.equal(result.status, 0, result.stdout); const args = JSON.parse(readFileSync(join(dir, 'bin/argv.json'))); + if (sequence === 'new-user') assert.ok(args.includes('tests/e2e/graph/semantic-lod.spec.ts')); assert.deepEqual(args.filter(arg => arg.startsWith('--trace=')), ['--trace=retain-on-failure']); assert.deepEqual(args.filter(arg => arg.startsWith('--retries=')), ['--retries=0']); assert.deepEqual(args.filter(arg => arg.startsWith('--workers=')), ['--workers=1']); diff --git a/tests/sequences/unified.config.mjs b/tests/sequences/unified.config.mjs index af661645..1cd4e35a 100644 --- a/tests/sequences/unified.config.mjs +++ b/tests/sequences/unified.config.mjs @@ -22,6 +22,7 @@ export const SEQUENCES = { 'tests/e2e/graph/overview-board-actions.spec.ts', 'tests/e2e/graph/recorded-ai-work.spec.ts', 'tests/e2e/graph/admin-dialog-responsive.spec.ts', + 'tests/e2e/graph/semantic-lod.spec.ts', 'tests/e2e/smoke/user-smoke.spec.ts', 'tests/e2e/quality/persona-lifecycle.spec.ts', 'tests/e2e/quality/graph-motion.spec.ts', From 97ea8ad11d7fbb61c83af29afd5535579d05cf69 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 09:23:38 -0700 Subject: [PATCH 06/14] docs: record semantic overview validation and remaining gates --- docs/README.md | 1 + docs/architecture/dynamic-graph-lod.md | 3 + .../DYNAMIC_LOD_VALIDATION_2026-10-27.md | 120 ++++++++++++++++++ .../images/dynamic-lod-20261027-desktop.png | Bin 0 -> 148572 bytes .../images/dynamic-lod-20261027-phone.png | Bin 0 -> 73254 bytes 5 files changed, 124 insertions(+) create mode 100644 docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md create mode 100644 docs/quality/images/dynamic-lod-20261027-desktop.png create mode 100644 docs/quality/images/dynamic-lod-20261027-phone.png diff --git a/docs/README.md b/docs/README.md index 129b6ef6..a765381e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ Welcome to the GraphDone documentation! This directory contains comprehensive gu - [Interaction Model — the friction-free contract](./design/interaction-model.md) - UX constitution: modes, exits, click budgets - [Progressive Streaming design](./design/progressive-streaming.md) - ADAPT-4: scale to huge graphs on slow links - [Dynamic graph LOD continuation](./architecture/dynamic-graph-lod.md) - Current evidence, semantic zoom, generic AI-authored systems, staged delivery and completion gates +- [First LOD slice: validation and screenshots](./quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md) - Owned desktop/phone fixtures, tested revisions and remaining gates - [CodeGraph LOD reuse](./architecture/codegraph-lod-reuse.md) - Working techniques, source references and regression contracts to share with GraphDone - [Testing & Refinement Plan](./TESTING_AND_REFINEMENT_PLAN.md) - The never-done loop; current cycle's verification debt - [AI Agents Quickstart](./api/AI_AGENTS.md) - 5-minute MCP/GraphQL setup for agent teammates diff --git a/docs/architecture/dynamic-graph-lod.md b/docs/architecture/dynamic-graph-lod.md index 2f3a9bfd..0bb4fa97 100644 --- a/docs/architecture/dynamic-graph-lod.md +++ b/docs/architecture/dynamic-graph-lod.md @@ -421,6 +421,9 @@ keep implementations independent. No shared libraries or common package. ## 8. First production-renderer slice (2026-10-27) +See the [validation record and screenshots](../quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md) +for exact revisions, passing checks, retained failures and remaining integration gates. + `packages/web/src/lib/semanticLOD.ts` now projects loaded source items into top-level groups. Precedence is explicit `metadata.lod.groupPath` (or `metadata.groupPath`), native containment, release `lane`, `kind`, then item type. diff --git a/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md b/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md new file mode 100644 index 00000000..0f11bf95 --- /dev/null +++ b/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md @@ -0,0 +1,120 @@ +# Semantic overview validation — 2026-10-27 + +The first production-renderer slice adds readable overview groups and member +navigation. It does not complete the [dynamic LOD delivery plan](../architecture/dynamic-graph-lod.md). +CodeGraph supplied ideas; both projects keep independent implementations and no +shared library was introduced. + +## Revisions and environment + +- Base: `ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`, also the live remote `dev` + when rechecked on 2026-10-27. +- Implementation: `df5d1cab932985092d77c17826af9d5cfc0e94b0`. +- Bidirectional readability refinement: `8ddd573c6edd433b9106a445b60e7c8b45009164`. +- Final validation/test-enrollment head: + `5f11f3bcc97fa3e90ffa1ea31f8e75f97a1ace16`. Its only application-source change + from `8ddd573` removes an unsupported lint-rule directive; the runtime behavior + is unchanged. Other changes enroll the browser tests in headed acceptance. +- Independent agent review found no outstanding findings at all three heads. + This is not a GitHub non-author approval. +- Node 22.18.0, npm 10.8.2; clean `npm ci --no-audit --no-fund` installed 1,221 + packages. Dependencies and lockfile remain unchanged from the reviewed base. +- Chrome 151.0.7922.71, headed under Xvfb, one browser worker and zero retries. + Phone coverage uses a 390×900 viewport and CDP touch emulation, not phone hardware. +- Owned Neo4j 5.26.12 container, built API on port 28527, isolated web on 28528. + Tests never seeded or edited the owner's application on port 8540. + +## Results + +| Check | Result and revision | +| --- | --- | +| `npm run typecheck` | Pass, five tasks at `5f11f3b` | +| `npm run lint` | Pass, four tasks at `5f11f3b`; existing warnings remain | +| `npm run build` | Pass, four tasks at `5f11f3b` | +| `npm run perf:bundle` | Pass: largest gzipped JS chunk 265.8kB / 450kB budget | +| `npm run test -w @graphdone/web -- --maxWorkers=1 --minWorkers=1` | 335/335 across 41 files at `5f11f3b` | +| `node --test tests/lib/runner/unified-cli.test.mjs` | 25/25, zero skipped at `5f11f3b`; verifies LOD enrollment in the blocking new-user sequence | +| `semantic-lod.spec.ts` | 2/2 at `8ddd573`, 1440×900 and 390×900, 1.9 minutes | +| Existing `graph-motion`, `graph-resize`, `graph-details` specs | 7/7 at `df5d1ca`, 3.5 minutes; subsequent behavior changes only adjust the ≥40-item overview threshold, which these small-graph fixtures do not enter | +| `npm audit --omit=dev --audit-level=high` | **Fails**: 5 moderate / 1 high in the unchanged dependency tree; high finding is Undici 6.28.1. No dependency upgrade was folded into this feature. | + +Run the focused browser suite with an owned stack and installed browser: + +```sh +TEST_URL=http://127.0.0.1:28528 QC_BROWSER_PORT=28529 \ + xvfb-run --auto-servernum npm run qc:lod +``` + +The two browser journeys use **301 pinned items and 370 relationships**, with +eight explicit top-level groups and one long multiline title. This matches the +reviewed localhost graph's cardinality, not its private topology or content. +Assertions cover: + +- 14px non-overlapping summary titles, complete group/member access including + phone overflow, and the original source item in the inspector. +- Parent coverage while zooming in through 1.1; item titles ≥12px at 1.2, then + while zooming out through 1.14 and 1.01; summaries return at 0.99. +- Real wheel input and mouse pan over summary cards; real CDP touch pan/pinch; + keyboard open/Escape, inspection, Back, and focus restoration. +- Forced Overview at normal item scale hides source groups and arrows from + painting/hit testing; source filters remain in D3-owned definitions. +- The same SVG survives representation changes within a graph. The existing app + may replace it when changing graphs. A→B→A starts a fresh inspection session + and Back restores the new entry camera exactly. +- Zero attempted GraphQL mutations during guarded navigation; exact source + positions and IDs before/after navigation and reload; zero page errors. + +The pure projection tests additionally conserve every directed, typed edge, +including parallel/reverse edges, internal links, self-loops and missing endpoints; +they exercise native containment, cycles, conflicting parents, malformed metadata, +stable membership and untouched canonical objects. + +## Screenshots + +These are synthetic owned fixtures captured from the production component. + +![Desktop semantic overview](images/dynamic-lod-20261027-desktop.png) + +![Phone semantic overview](images/dynamic-lod-20261027-phone.png) + +## Failures retained and corrections + +Local raw evidence remains in the owned worktree's ignored +`test-artifacts/lod-evidence/`. The records include: + +1. A watched API restarted during a concurrent Core rebuild, and 100-input + Neo4j create batches exceeded fixture setup time. Use the stable built API + and repeated 20-input batches; retain the full 301/370 dataset. +2. Member selection moved the camera but lost the inspector when the detached + click target reached the document outside-click handler. The overview stops + click propagation; both viewport regressions subsequently passed. +3. A default-camera refresh on startup could prevent initial fit. Refresh title + geometry only when leaving Overview in the same graph scope. +4. The graph-switch test initially assumed its selector closed automatically; + the existing selector intentionally stays open. The test now closes it with + Escape. A later assertion incorrectly demanded the same SVG across different + graphs; identity is now checked within each graph's LOD navigation session. +5. The unconstrained parallel unit run had a 16ms timing assertion fail and a + 5s UI timeout under load. Sequential workers passed all 335 tests, with the + original failures retained. +6. Final lint rejected a suppression for a plugin rule this repo does not load. + Removing that directive fixed lint without changing runtime behavior. + +See the [renderer-ownership lesson](../../.agents/inbox/2026-10-27-lod-projections-need-renderer-ownership.md) +for causes and corrective rules. + +## Cleanup and remaining gates + +The owned API/web processes, Neo4j container and anonymous volumes were removed. +Owned SQLite authentication/operations databases were removed after process exit. +The owner's original localhost app still returned HTTP 200 afterward. Other +worktrees, CodeGraph source, main, deployments and repository protections were +unchanged by this work. + +This is not merge or release readiness: the production audit is red, full remote +branch CI and broader integration/image/installer gates have not been established, +and no deployment has been performed. This slice still constructs the complete +source SVG and loads full graph snapshots. Nested spatial expansion, local density +budgets, summary-first loading, Cloud parity and measured 1k/10k capacity remain +on the delivery plan. Existing autosave of newly laid-out/overlapping source nodes +is not fixed by the zero-write test on pinned, non-overlapping fixtures. diff --git a/docs/quality/images/dynamic-lod-20261027-desktop.png b/docs/quality/images/dynamic-lod-20261027-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..6c754d621fe6eb964a6b03a3f884bdb7b8753ade GIT binary patch literal 148572 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 z zF#%VliJH)4PM!ZLU9}(^Z2!~a?o3*ti4U`L*ymT-mG2>;0t@Q};`v;!bp7hwLn`_h zMp(lQv&K!6RXiSRs@EqG9>|3O0Bjrwk01_Lv0G> z(0YM)#rBF>m^+GB=%m!7^Y`-c@KvTaG&gjn<=PG??gsU!+xsve{-#fTH;_P9p%Vqa z<`@gZPHkwszGZWlUI0T=Q6npenKTU%U^+EHP<={+#%AZBU&`+}v6nD0U~_|wAu-Z|%3tf#IC+$B&>i+mOu&P6UwIiEwK1bIooGW7x450qf%aPay9lQdrZ8lLsz zOFN%tRwfs>G_y-_$I@3XtI3butKPXn#}`H1IvGR&dC+$TQuQ2uE`dS*xK&eAZwLUiv&%pZHpKw|H=tMJ-}OKL?m$^{0&Lo) zQ8D3}Z_}Cg{`kFOY9M-ez&RrZuqKxzH5Rn0l-=Ors+2tN`su(9E}=s6tY*76Ww+dx z$pC{Qn5(F%g@uK!FRgibPKJ5ForuFNc{y2G2sk{x9q!@xp*wm3H)2<$u*1_@)TcJq z{fR-3V$d5(sGw|+WNG0C0n^u+pFF{^LnHtO)8k-?{=UA(0M`SnoaHD@{tB`WTkgEq z>;l8_!PfbNjTtLJ6qQnG+I!Nuq;ukUZUSrz*k)=rl}cxt&Q#6f;}y(R4zG=iGt$?` zOC==_STeo9)}{c#d}L8qwrN?Oj#xXmV=7PrpjD~}KAtD5_uF}nMwKjl~_siHNnG* zR<4cM=!>37ei#dJaryZ(@_2(8r0}K)Am$Z9H!htXY>rBT*eIU3PCgv0rinju>OOiG zFqLU9zRjzl@hMs*S~SZ2^W6S4kL(W5)|tyz%YjnVZYKu@kFQ#ml{8~w6}5xdQjqywvjy1m0aEB^s{*?d&axdfGY{Ua-a%#a~OBk#$xgb<1{j#>Fpk7k`g| zn;X1KZ&#a^6D&Gob*sJI}+~yj9bpkB$hTk;DcOC8dNVVW-8k zCz?Hh3YTu$<#;!q@;=tk)cj*dI$rtR@ZjLUh$$dO>CK>tEi@Er+9Pseuy}QR-DYre zzd3<7Z%J0TBlqfWH?q0AfTIe<1&%;gI-u z*`ES^EHMeOM$4MZ@zTGxCvzNm&d)|bAaCA4+rUHP;~!>+tcmi*i~YIUPOb0#(!L-_ zn}(Hl7&l6j6^lE9C!-igTk_g~o0>-^+xzEXV_l#21lBP_r(XMou~lb5Uz=!3qm>A0 z)GOQHo8ImQLmlfIN==&!^%oGu6EDlb4vXIdwO6MqP-0?LLh1zc zL{ZzLyLX>;!Qu+?p5YC7WRs9x=9sg?70cPLhw10)9W%{B^YU0>UCRmkXd&%HRWSL^ z^a*;|(|;b($%#l+*0YK@w)VI zY0eKCo^ax+HX`X^RjcC^iX5mac?)BqeZKjU2;r}s^RG&3Q~SwOIyI9L)-xd#lxp7} ztOM08>i0Z)b=p8)O=R8ay*SyEn%59O_ijj2H<9qo zhbdO``pwt26c%z8k)LD~ZLW4v8dBN2D#Z zSA1?YEWB;^rMQCjbn7%1Td;7F(2vG=M`}oAz?%i_jw6wCveddL1is-KzjtW%X*UO@ zyv-2L{>`V2#bHqOv^~QuDqR?6WaR$K7?Yx6ndj+r+T;ZIP(?H!`xrKX+Q6BbKHqSS zgu^GmN>Ndwg8o(PR~FZ4yA@RG)`1xJQ>3+pQ5l(VMM!enGZ{O_75<1MauB?MOGqXB z5rrOgCF z)@yP=1q@^^;y>b5Qe|XTRVxtz3#~ky+u7+gzZw~FGl)R=vfqs8Ni&BDT+|4*PGR<) zc;%|H*Jk4zjx(PnQg_`KT3uW0!;TG?a94F#_usvvJUGf<7Tu!}EW^QH?GJDd#m7o2 zi{`7p`#ahHT$}$u8R>>y(f9A6dN&$rh0;jN%cf$C>1J4Z8C@nj0KEnV+()}5!s7EX zVqg!z<`R#m!5b4HpqTymx1 Date: Tue, 27 Oct 2026 09:24:51 -0700 Subject: [PATCH 07/14] docs: clarify resolved review follow-ups --- docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md b/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md index 0f11bf95..f8cb6a33 100644 --- a/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md +++ b/docs/quality/DYNAMIC_LOD_VALIDATION_2026-10-27.md @@ -15,8 +15,9 @@ shared library was introduced. `5f11f3bcc97fa3e90ffa1ea31f8e75f97a1ace16`. Its only application-source change from `8ddd573` removes an unsupported lint-rule directive; the runtime behavior is unchanged. Other changes enroll the browser tests in headed acceptance. -- Independent agent review found no outstanding findings at all three heads. - This is not a GitHub non-author approval. +- Independent review found no blocking findings at `df5d1ca`; its two nonblocking + follow-ups were addressed in `8ddd573`, and subsequent code reviews had no + findings. This is not a GitHub non-author approval. - Node 22.18.0, npm 10.8.2; clean `npm ci --no-audit --no-fund` installed 1,221 packages. Dependencies and lockfile remain unchanged from the reviewed base. - Chrome 151.0.7922.71, headed under Xvfb, one browser worker and zero retries. From 4abce30030e45e1e9d468c67e414302f270d45e9 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 09:28:44 -0700 Subject: [PATCH 08/14] docs: record draft PR client permission evidence --- ...0-27-github-connector-write-permissions.md | 38 +++++++++++++++++++ .agents/issues/INDEX.md | 1 + 2 files changed, 39 insertions(+) create mode 100644 .agents/inbox/2026-10-27-github-connector-write-permissions.md diff --git a/.agents/inbox/2026-10-27-github-connector-write-permissions.md b/.agents/inbox/2026-10-27-github-connector-write-permissions.md new file mode 100644 index 00000000..e2685ccb --- /dev/null +++ b/.agents/inbox/2026-10-27-github-connector-write-permissions.md @@ -0,0 +1,38 @@ +# A GitHub connector denial describes that client, not every configured client + +Date: 2026-10-27. Context: publishing the owner-authorized GraphDone LOD work as +a draft PR to `dev`. Searched the symptom index and the existing live-ref/transport +lesson before recording this distinct create-PR case. + +## Evidence + +The feature branch push succeeded through the existing SSH remote. The GitHub +connector's `create_pull_request` then returned: + +```text +HTTP 403: Resource not accessible by integration +``` + +The existing CLI returned `mvalancy` for `gh api user --jq .login` and successfully +ran `gh pr create --repo GraphDone/GraphDone-Core --base dev --head +feature/dynamic-lod-overview-20261027 --draft --body-file `. +Readback confirmed [PR #192](https://github.com/GraphDone/GraphDone-Core/pull/192) +was open, draft, and pointed at the intended source head. No authentication, +account, token scope or repository protection was changed. No merge occurred. + +## Cause and corrective rule + +The integration could read this repository but could not create the PR. Its +permission boundary differs from the already configured CLI credential. Report +that specific limitation rather than declaring all GitHub access blocked or +requesting broader credentials. For an already-authorized operation, an existing +client with its own valid access can be used; verify the resulting object and +source head. Do not use another client to evade repository policy, an account +denial, an approval rejection or an operation outside the user's authorization. + +## Limits + +This establishes one successful draft-PR creation through existing credentials. +It does not establish write access for other repositories, non-author approval, +merge readiness or deployment authority. The narrower admin-integration playbook +still governs merges; this incident does not amend that policy. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 8ccd0c5e..fd923e29 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| GitHub connector can read a repo but PR creation returns `Resource not accessible by integration` | [Client-specific write permissions](../inbox/2026-10-27-github-connector-write-permissions.md); verify existing authorized access without changing credentials or policy | | LOD inspection moves the camera but loses selection; hidden source controls or nested SVG definitions leak across renderers | [Display projections need explicit renderer ownership](../inbox/2026-10-27-lod-projections-need-renderer-ownership.md); `semantic-lod.spec.ts`, `semanticLOD.test.ts` | | A new LOD design overlooks CodeGraph or treats an unused helper as a shipped shared engine | [CodeGraph reuse map](../../docs/architecture/codegraph-lod-reuse.md); [consumer-evidence lesson](../inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md) | | Dense graph loses all names at fit scale; an old LOD preview is mistaken for shipped behavior | [Dynamic graph LOD continuation](../../docs/architecture/dynamic-graph-lod.md); current state, staged delivery and explicit completion gates | From bfcb71f9a656d60bb1a4f5807d107228fc8cc737 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 12:04:08 -0700 Subject: [PATCH 09/14] Separate cold LOD fixture budgets and enforce owned cleanup --- ...cold-lod-fixtures-need-separate-budgets.md | 45 +++++ .agents/issues/INDEX.md | 1 + tests/e2e/graph/semantic-lod.spec.ts | 157 +++++++++++------- tests/lib/seedGraph.ts | 12 +- 4 files changed, 153 insertions(+), 62 deletions(-) create mode 100644 .agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md diff --git a/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md new file mode 100644 index 00000000..5860189e --- /dev/null +++ b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md @@ -0,0 +1,45 @@ +# Cold database fixture setup must not consume the browser interaction budget + +Date: 2026-10-27. Searched the symptom index and the existing LOD ownership and +performance-evidence lessons. Final PR #192 CI at +`4abce30030e45e1e9d468c67e414302f270d45e9` failed the strict headed new-user +sequence even though the earlier focused local browser run passed. + +## Evidence and cause + +```sh +gh run view 40541503856 --repo GraphDone/GraphDone-Core --job 122322370157 --log-failed +gh run download 40541503856 --repo GraphDone/GraphDone-Core \ + --name smoke-results-e2599a1245884c450fd1635d89b92cc60871a033 --dir +``` + +The basic smoke sequence passed 6/6. New-user reported 43 pass / 2 fail: one +desktop LOD case hit its 120-second whole-test timeout, and the second entry +was the harness's process-exit failure. The phone LOD case passed in 55.3s. +The trace records eight fixture mutation calls taking approximately 5.7–11.6s +each. Those calls consumed most of the shared setup/interaction budget on this +cold database. Navigation, graph switching and camera-restoration assertions +completed; the timeout interrupted the final source-position readback. That is +an incomplete persistence check, not a passing journey or proof of a renderer +hang. Cold query planning is consistent with the earlier local batch-size +incident; this trace does not separately profile the database planner. + +## Corrective rule + +Use a bounded Playwright fixture for expensive real API setup and teardown. +The LOD fixture gets 180 seconds per fixture phase; the interaction test keeps +120 seconds, 10-second actions and its existing assertion timeouts. Preserve +the 301-item / 370-edge graph, all assertions, and zero retries. Register owned +graph IDs immediately after creation so partial setup can be cleaned. Teardown +runs before the page closes, removes the write guard, deletes edges before +items/graph, verifies graph removal, and surfaces cleanup failures. A teardown +failure must fail the test, not silently leave fixtures behind. + +## Limits + +This isolates fixture provisioning from interaction correctness; it neither +improves measured application speed nor proves the large-graph performance +target. Rerun the complete strict CI sequence and record its actual result. +Raw CI artifacts are retained under the owned resolution evidence directory; +they are not added to the public repository because traces can contain fixture +sessions. The run link remains the canonical CI failure evidence. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index fd923e29..9327d61b 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| Dense-graph browser case exhausts its timeout after slow API fixture creation; cleanup errors disappear | [Bound fixture setup separately and fail on cleanup errors](../inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md); `tests/e2e/graph/semantic-lod.spec.ts` | | GitHub connector can read a repo but PR creation returns `Resource not accessible by integration` | [Client-specific write permissions](../inbox/2026-10-27-github-connector-write-permissions.md); verify existing authorized access without changing credentials or policy | | LOD inspection moves the camera but loses selection; hidden source controls or nested SVG definitions leak across renderers | [Display projections need explicit renderer ownership](../inbox/2026-10-27-lod-projections-need-renderer-ownership.md); `semantic-lod.spec.ts`, `semanticLOD.test.ts` | | A new LOD design overlooks CodeGraph or treats an unused helper as a shipped shared engine | [CodeGraph reuse map](../../docs/architecture/codegraph-lod-reuse.md); [consumer-evidence lesson](../inbox/2026-10-27-lod-reuse-needs-live-consumer-evidence.md) | diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index 862555c3..4a728d2b 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -1,60 +1,108 @@ -import { test, expect } from '@playwright/test'; +import { test as base, expect, type CDPSession } from '@playwright/test'; import { login, TEST_USERS } from '../../lib/auth'; import { gql } from '../../lib/graphRequest'; import { seedLargeGraph, deleteGraphDeep } from '../../lib/seedGraph'; -test.use({ actionTimeout: 10000 }); - -for (const width of [1440, 390]) - test(`semantic overview preserves 301 items and layout at ${width}px @lod`, async ({ - page, - }, info) => { - test.setTimeout(120000); - await page.setViewportSize({ width, height: 900 }); - const touch = - width === 390 ? await page.context().newCDPSession(page) : null; - if (touch) - await touch.send('Emulation.setTouchEmulationEnabled', { - enabled: true, - maxTouchPoints: 2, - }); - await login(page, TEST_USERS.ADMIN); - const seeded = await seedLargeGraph(page, { - size: 301, - spacing: 300, - edgeFactor: 370 / 301, - namePrefix: 'Semantic LOD', - batchSize: 20, - titleForIndex: (index, type) => - index === 8 - ? `${type} 8 — Follow a request through gateway validation, event delivery, retries, settlement and reconciliation across the entire system` - : `${type} ${index}`, - metadataForIndex: index => ({ - lod: { groupPath: [`Subsystem ${index % 8}`, 'Worker'] }, - }), - }); - const attemptedWrites: string[] = []; - let otherGraph: Awaited> | undefined; - const pageErrors: string[] = []; - page.on('pageerror', error => pageErrors.push(error.message)); - const snapshot = async () => - ( - await gql( - page, - `query($id:ID!){workItems(where:{graph:{id:$id}}){id positionX positionY}}`, - { id: seeded.graphId } - ) - ).workItems.sort((a: { id: string }, b: { id: string }) => - a.id.localeCompare(b.id) - ); - try { - if (width === 1440) - otherGraph = await seedLargeGraph(page, { - size: 40, +// Cold Neo4j query planning belongs to fixture setup, not the interaction +// budget. Both budgets are bounded; slow clicks and assertions still fail. +const test = base.extend<{ + lodFixture: { + seeded: Awaited>; + otherGraph?: Awaited>; + touch: CDPSession | null; + }; +}>({ + lodFixture: [ + async ({ page }, use) => { + const width = page.viewportSize()!.width; + const ownedGraphIds: string[] = []; + const touch = + width === 390 ? await page.context().newCDPSession(page) : null; + try { + if (touch) + await touch.send('Emulation.setTouchEmulationEnabled', { + enabled: true, + maxTouchPoints: 2, + }); + await login(page, TEST_USERS.ADMIN); + const seeded = await seedLargeGraph(page, { + size: 301, spacing: 300, + edgeFactor: 370 / 301, + namePrefix: 'Semantic LOD', batchSize: 20, - namePrefix: 'LOD switch target', + onGraphCreated: id => ownedGraphIds.push(id), + titleForIndex: (index, type) => + index === 8 + ? `${type} 8 — Follow a request through gateway validation, event delivery, retries, settlement and reconciliation across the entire system` + : `${type} ${index}`, + metadataForIndex: index => ({ + lod: { groupPath: [`Subsystem ${index % 8}`, 'Worker'] }, + }), }); + const otherGraph = + width === 1440 + ? await seedLargeGraph(page, { + size: 40, + spacing: 300, + batchSize: 20, + namePrefix: 'LOD switch target', + onGraphCreated: id => ownedGraphIds.push(id), + }) + : undefined; + await use({ seeded, otherGraph, touch }); + } finally { + // Fixture teardown runs before Playwright closes its page, including on + // assertion failure. Surface cleanup failures instead of swallowing them. + await page.unroute('**/api/graphql'); + const cleanup = await Promise.allSettled( + ownedGraphIds.map(async id => { + await deleteGraphDeep(page, id, { strict: true }); + const result = await gql( + page, + 'query($id:ID!){graphs(where:{id:$id}){id}}', + { id } + ); + expect(result.graphs).toEqual([]); + }) + ); + await touch?.detach(); + const failures = cleanup.filter(result => result.status === 'rejected'); + if (failures.length) + throw new AggregateError( + failures.map(result => result.reason), + 'LOD fixture cleanup failed' + ); + } + }, + { timeout: 180000 }, + ], +}); + +test.use({ actionTimeout: 10000 }); + +for (const width of [1440, 390]) + test.describe(`${width}px LOD`, () => { + test.use({ viewport: { width, height: 900 } }); + test(`semantic overview preserves 301 items and layout at ${width}px @lod`, async ({ + page, + lodFixture, + }, info) => { + test.setTimeout(120000); + const { seeded, otherGraph, touch } = lodFixture; + const attemptedWrites: string[] = []; + const pageErrors: string[] = []; + page.on('pageerror', error => pageErrors.push(error.message)); + const snapshot = async () => + ( + await gql( + page, + `query($id:ID!){workItems(where:{graph:{id:$id}}){id positionX positionY}}`, + { id: seeded.graphId } + ) + ).workItems.sort((a: { id: string }, b: { id: string }) => + a.id.localeCompare(b.id) + ); const graphNames = (await gql(page, 'query{graphs{id name}}')).graphs as { id: string; name: string; @@ -432,12 +480,5 @@ for (const width of [1440, 390]) await expect(page.locator('.graph-container .node')).toHaveCount(301); expect(await snapshot()).toEqual(before); expect(attemptedWrites).toEqual([]); - } finally { - if (!page.isClosed()) { - await page.unroute('**/api/graphql'); - await deleteGraphDeep(page, seeded.graphId); - if (otherGraph) await deleteGraphDeep(page, otherGraph.graphId); - } - await touch?.detach(); - } + }); }); diff --git a/tests/lib/seedGraph.ts b/tests/lib/seedGraph.ts index 5e611c60..938c8e91 100644 --- a/tests/lib/seedGraph.ts +++ b/tests/lib/seedGraph.ts @@ -59,6 +59,8 @@ export interface SeedOptions { titleForIndex?: (index: number, type: string) => string; /** Smaller repeated mutation shapes avoid Neo4j planning hundreds of creates. */ batchSize?: number; + /** Register ownership before node/edge setup, so partial setup can be cleaned. */ + onGraphCreated?: (graphId: string) => void; } export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise { @@ -72,6 +74,7 @@ export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise { +export async function deleteGraphDeep(page: Page, graphId: string, options: { strict?: boolean } = {}): Promise { + const handleFailure = (error: unknown) => { if (options.strict) throw error; }; // Edges first (orphan edges break the edges query), then nodes, then graph. await gql( page, `mutation($id: ID!) { deleteEdges(where: { source: { graph: { id: $id } } }) { nodesDeleted } }`, { id: graphId } - ).catch(() => {}); + ).catch(handleFailure); await gql( page, `mutation($id: ID!) { deleteWorkItems(where: { graph: { id: $id } }) { nodesDeleted } }`, { id: graphId } - ).catch(() => {}); - await gql(page, `mutation($id: ID!) { deleteGraphs(where: { id: $id }) { nodesDeleted } }`, { id: graphId }).catch(() => {}); + ).catch(handleFailure); + await gql(page, `mutation($id: ID!) { deleteGraphs(where: { id: $id }) { nodesDeleted } }`, { id: graphId }).catch(handleFailure); } From 1868fa73ed6f0a5cdc93a614ce25573aac89971c Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 12:09:52 -0700 Subject: [PATCH 10/14] Reserve independent cleanup budget after LOD setup timeout --- ...cold-lod-fixtures-need-separate-budgets.md | 12 +++- tests/e2e/graph/semantic-lod.spec.ts | 28 ++------- tests/lib/ownedGraphIdsFixture.ts | 34 +++++++++++ tests/lib/runner/owned-graph-fixture.test.mjs | 57 +++++++++++++++++++ 4 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 tests/lib/ownedGraphIdsFixture.ts create mode 100644 tests/lib/runner/owned-graph-fixture.test.mjs diff --git a/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md index 5860189e..5e340ea3 100644 --- a/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md +++ b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md @@ -27,14 +27,22 @@ incident; this trace does not separately profile the database planner. ## Corrective rule Use a bounded Playwright fixture for expensive real API setup and teardown. -The LOD fixture gets 180 seconds per fixture phase; the interaction test keeps -120 seconds, 10-second actions and its existing assertion timeouts. Preserve +The LOD setup fixture gets 180 seconds; the interaction test keeps +120 seconds, 10-second actions and its existing assertion timeouts. An ownership +dependency fixture has an independent 60-second cleanup budget. Preserve the 301-item / 370-edge graph, all assertions, and zero retries. Register owned graph IDs immediately after creation so partial setup can be cleaned. Teardown runs before the page closes, removes the write guard, deletes edges before items/graph, verifies graph removal, and surfaces cleanup failures. A teardown failure must fail the test, not silently leave fixtures behind. +Independent review caught a flaw in the first correction: Playwright 1.63 shares +a fixture's timeout slot between setup and teardown and can skip teardown after +setup exhausts it. Moving ownership/cleanup into a dependency fixture keeps its +budget independent. Do not describe one fixture timeout as a separate allowance +for each phase. The harness negative control must intentionally time out its +dependent setup and still observe strict edge/item/graph cleanup and readback. + ## Limits This isolates fixture provisioning from interaction correctness; it neither diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index 4a728d2b..c1e811bb 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -1,21 +1,23 @@ import { test as base, expect, type CDPSession } from '@playwright/test'; import { login, TEST_USERS } from '../../lib/auth'; import { gql } from '../../lib/graphRequest'; -import { seedLargeGraph, deleteGraphDeep } from '../../lib/seedGraph'; +import { seedLargeGraph } from '../../lib/seedGraph'; +import { ownedGraphIdsFixture } from '../../lib/ownedGraphIdsFixture'; // Cold Neo4j query planning belongs to fixture setup, not the interaction // budget. Both budgets are bounded; slow clicks and assertions still fail. const test = base.extend<{ + ownedGraphIds: string[]; lodFixture: { seeded: Awaited>; otherGraph?: Awaited>; touch: CDPSession | null; }; }>({ + ownedGraphIds: [ownedGraphIdsFixture, { timeout: 60000 }], lodFixture: [ - async ({ page }, use) => { + async ({ page, ownedGraphIds }, use) => { const width = page.viewportSize()!.width; - const ownedGraphIds: string[] = []; const touch = width === 390 ? await page.context().newCDPSession(page) : null; try { @@ -52,27 +54,7 @@ const test = base.extend<{ : undefined; await use({ seeded, otherGraph, touch }); } finally { - // Fixture teardown runs before Playwright closes its page, including on - // assertion failure. Surface cleanup failures instead of swallowing them. - await page.unroute('**/api/graphql'); - const cleanup = await Promise.allSettled( - ownedGraphIds.map(async id => { - await deleteGraphDeep(page, id, { strict: true }); - const result = await gql( - page, - 'query($id:ID!){graphs(where:{id:$id}){id}}', - { id } - ); - expect(result.graphs).toEqual([]); - }) - ); await touch?.detach(); - const failures = cleanup.filter(result => result.status === 'rejected'); - if (failures.length) - throw new AggregateError( - failures.map(result => result.reason), - 'LOD fixture cleanup failed' - ); } }, { timeout: 180000 }, diff --git a/tests/lib/ownedGraphIdsFixture.ts b/tests/lib/ownedGraphIdsFixture.ts new file mode 100644 index 00000000..62e1ca0a --- /dev/null +++ b/tests/lib/ownedGraphIdsFixture.ts @@ -0,0 +1,34 @@ +import { expect, type Page } from '@playwright/test'; +import { gql } from './graphRequest'; +import { deleteGraphDeep } from './seedGraph'; + +/** Keep cleanup in a dependency fixture so slow/timed-out seeding cannot consume + * its timeout slot. Register each graph immediately after its creation. */ +export async function ownedGraphIdsFixture( + { page }: { page: Page }, + use: (ids: string[]) => Promise +): Promise { + const ids: string[] = []; + try { + await use(ids); + } finally { + await page.unroute('**/api/graphql'); + const cleanup = await Promise.allSettled( + ids.map(async id => { + await deleteGraphDeep(page, id, { strict: true }); + const result = await gql( + page, + 'query($id:ID!){graphs(where:{id:$id}){id}}', + { id } + ); + expect(result.graphs).toEqual([]); + }) + ); + const failures = cleanup.filter(result => result.status === 'rejected'); + if (failures.length) + throw new AggregateError( + failures.map(result => result.reason), + 'Owned graph cleanup failed' + ); + } +} diff --git a/tests/lib/runner/owned-graph-fixture.test.mjs b/tests/lib/runner/owned-graph-fixture.test.mjs new file mode 100644 index 00000000..4e3e402a --- /dev/null +++ b/tests/lib/runner/owned-graph-fixture.test.mjs @@ -0,0 +1,57 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync, symlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const root = fileURLToPath(new URL('../../../', import.meta.url)); + +for (const scenario of ['setup-timeout', 'cleanup-failure']) { + test(`owned graph cleanup remains strict after ${scenario}`, t => { + const dir = mkdtempSync(join(tmpdir(), 'graphdone-fixture-negative-')); + t.after(() => rmSync(dir, { recursive: true, force: true })); + symlinkSync(join(root, 'node_modules'), join(dir, 'node_modules'), 'dir'); + const events = join(dir, 'events.json'); + writeFileSync(join(dir, 'playwright.config.ts'), 'export default { testDir: ".", retries: 0, workers: 1, reporter: "json", timeout: 5000 };'); + writeFileSync(join(dir, 'negative.spec.ts'), ` +import { test as base } from '@playwright/test'; +import { writeFileSync } from 'node:fs'; +import { ownedGraphIdsFixture } from ${JSON.stringify(join(root, 'tests/lib/ownedGraphIdsFixture.ts'))}; +const calls = []; +const fakePage = { + async unroute() { calls.push('unroute'); }, + async evaluate(_callback, { query, variables }) { + if (variables.id !== 'owned-graph') throw new Error('Unexpected graph ownership'); + const operation = ['deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('].find(name => query.includes(name)); + calls.push(operation); + if (${JSON.stringify(scenario)} === 'cleanup-failure' && operation === 'deleteEdges') throw new Error('Owned deletion failed'); + return { graphs: [] }; + }, +}; +const test = base.extend({ + page: async ({}, use) => { try { await use(fakePage); } finally { writeFileSync(${JSON.stringify(events)}, JSON.stringify(calls)); } }, + ownedGraphIds: [ownedGraphIdsFixture, { timeout: 3000 }], + setup: [async ({ ownedGraphIds }, use) => { + ownedGraphIds.push('owned-graph'); + if (${JSON.stringify(scenario)} === 'setup-timeout') await new Promise(() => {}); + await use(true); + }, { timeout: 100 }], +}); +test('intentional negative control', async ({ setup }) => {}); +`); + const result = spawnSync(process.execPath, [join(root, 'node_modules/@playwright/test/cli.js'), 'test', '--config', join(dir, 'playwright.config.ts')], { + cwd: dir, env: { ...process.env, FORCE_COLOR: '0' }, encoding: 'utf8', timeout: 20000, + }); + assert.equal(result.status, 1, result.stderr || result.stdout); + const report = JSON.parse(result.stdout); + assert.equal(report.stats.unexpected, 1, result.stdout); + assert.equal(report.stats.skipped, 0); + assert.deepEqual(JSON.parse(readFileSync(events, 'utf8')), scenario === 'setup-timeout' + ? ['unroute', 'deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('] + : ['unroute', 'deleteEdges']); + const errors = report.suites[0].specs[0].tests[0].results[0].errors.map(error => error.message).join('\n'); + assert.match(errors, scenario === 'setup-timeout' ? /Fixture "setup" timeout/ : /Owned graph cleanup failed/); + }); +} From a051271094dc96be8c5746528ecd6fb36b470452 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 12:13:18 -0700 Subject: [PATCH 11/14] Drain late seed responses before cleaning owned graphs --- ...cold-lod-fixtures-need-separate-budgets.md | 9 +++++ tests/e2e/graph/semantic-lod.spec.ts | 13 +++--- tests/lib/ownedGraphIdsFixture.ts | 40 +++++++++++++++++-- tests/lib/runner/owned-graph-fixture.test.mjs | 19 ++++++--- tests/lib/seedGraph.ts | 6 +++ 5 files changed, 72 insertions(+), 15 deletions(-) diff --git a/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md index 5e340ea3..7e314b35 100644 --- a/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md +++ b/.agents/inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md @@ -43,6 +43,15 @@ budget independent. Do not describe one fixture timeout as a separate allowance for each phase. The harness negative control must intentionally time out its dependent setup and still observe strict edge/item/graph cleanup and readback. +The next review identified a late-response race: Playwright timeout does not +cancel an in-flight create. Cleanup now first aborts future seed batches and +drains tracked seed promises, retaining the response's graph ID before deletion. +The ownership fixture rejects new seed work once cleanup starts. A deferred-create +negative control releases the response after setup timeout and checks that no +item creation follows and the late graph is still deleted. A request that never +settles can exhaust cleanup's independent budget; that is a reported failed +cleanup, not a claim that remote cancellation or lost-ack recovery was proved. + ## Limits This isolates fixture provisioning from interaction correctness; it neither diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index c1e811bb..df2c3413 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -2,12 +2,15 @@ import { test as base, expect, type CDPSession } from '@playwright/test'; import { login, TEST_USERS } from '../../lib/auth'; import { gql } from '../../lib/graphRequest'; import { seedLargeGraph } from '../../lib/seedGraph'; -import { ownedGraphIdsFixture } from '../../lib/ownedGraphIdsFixture'; +import { + ownedGraphIdsFixture, + type OwnedGraphs, +} from '../../lib/ownedGraphIdsFixture'; // Cold Neo4j query planning belongs to fixture setup, not the interaction // budget. Both budgets are bounded; slow clicks and assertions still fail. const test = base.extend<{ - ownedGraphIds: string[]; + ownedGraphIds: OwnedGraphs; lodFixture: { seeded: Awaited>; otherGraph?: Awaited>; @@ -27,13 +30,12 @@ const test = base.extend<{ maxTouchPoints: 2, }); await login(page, TEST_USERS.ADMIN); - const seeded = await seedLargeGraph(page, { + const seeded = await ownedGraphIds.seed({ size: 301, spacing: 300, edgeFactor: 370 / 301, namePrefix: 'Semantic LOD', batchSize: 20, - onGraphCreated: id => ownedGraphIds.push(id), titleForIndex: (index, type) => index === 8 ? `${type} 8 — Follow a request through gateway validation, event delivery, retries, settlement and reconciliation across the entire system` @@ -44,12 +46,11 @@ const test = base.extend<{ }); const otherGraph = width === 1440 - ? await seedLargeGraph(page, { + ? await ownedGraphIds.seed({ size: 40, spacing: 300, batchSize: 20, namePrefix: 'LOD switch target', - onGraphCreated: id => ownedGraphIds.push(id), }) : undefined; await use({ seeded, otherGraph, touch }); diff --git a/tests/lib/ownedGraphIdsFixture.ts b/tests/lib/ownedGraphIdsFixture.ts index 62e1ca0a..b39be606 100644 --- a/tests/lib/ownedGraphIdsFixture.ts +++ b/tests/lib/ownedGraphIdsFixture.ts @@ -1,17 +1,51 @@ import { expect, type Page } from '@playwright/test'; import { gql } from './graphRequest'; -import { deleteGraphDeep } from './seedGraph'; +import { + deleteGraphDeep, + seedLargeGraph, + type SeedOptions, + type SeededGraph, +} from './seedGraph'; + +export interface OwnedGraphs { + seed: ( + options: Omit + ) => Promise; +} /** Keep cleanup in a dependency fixture so slow/timed-out seeding cannot consume * its timeout slot. Register each graph immediately after its creation. */ export async function ownedGraphIdsFixture( { page }: { page: Page }, - use: (ids: string[]) => Promise + use: (owner: OwnedGraphs) => Promise ): Promise { const ids: string[] = []; + const controller = new AbortController(); + const pending = new Set>(); + const owner: OwnedGraphs = { + seed(options) { + controller.signal.throwIfAborted(); + const task = seedLargeGraph(page, { + ...options, + signal: controller.signal, + onGraphCreated: id => ids.push(id), + }); + pending.add(task); + void task.then( + () => pending.delete(task), + () => pending.delete(task) + ); + return task; + }, + }; try { - await use(ids); + await use(owner); } finally { + // Playwright's timeout does not cancel async setup. Stop later batches, then + // drain in-flight responses before taking the ownership snapshot. Never + // abort a create response and lose the server-generated graph ID. + controller.abort(); + await Promise.allSettled([...pending]); await page.unroute('**/api/graphql'); const cleanup = await Promise.allSettled( ids.map(async id => { diff --git a/tests/lib/runner/owned-graph-fixture.test.mjs b/tests/lib/runner/owned-graph-fixture.test.mjs index 4e3e402a..198ed048 100644 --- a/tests/lib/runner/owned-graph-fixture.test.mjs +++ b/tests/lib/runner/owned-graph-fixture.test.mjs @@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process'; const root = fileURLToPath(new URL('../../../', import.meta.url)); -for (const scenario of ['setup-timeout', 'cleanup-failure']) { +for (const scenario of ['setup-timeout', 'late-create-response', 'cleanup-failure']) { test(`owned graph cleanup remains strict after ${scenario}`, t => { const dir = mkdtempSync(join(tmpdir(), 'graphdone-fixture-negative-')); t.after(() => rmSync(dir, { recursive: true, force: true })); @@ -23,6 +23,13 @@ const calls = []; const fakePage = { async unroute() { calls.push('unroute'); }, async evaluate(_callback, { query, variables }) { + if (query.includes(' me ')) return { me: { id: 'owned-user' } }; + if (query.includes('createGraphs')) { + calls.push('createRequested'); + if (${JSON.stringify(scenario)} === 'late-create-response') await new Promise(resolve => setTimeout(resolve, 250)); + calls.push('created'); + return { createGraphs: { graphs: [{ id: 'owned-graph' }] } }; + } if (variables.id !== 'owned-graph') throw new Error('Unexpected graph ownership'); const operation = ['deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('].find(name => query.includes(name)); calls.push(operation); @@ -34,7 +41,7 @@ const test = base.extend({ page: async ({}, use) => { try { await use(fakePage); } finally { writeFileSync(${JSON.stringify(events)}, JSON.stringify(calls)); } }, ownedGraphIds: [ownedGraphIdsFixture, { timeout: 3000 }], setup: [async ({ ownedGraphIds }, use) => { - ownedGraphIds.push('owned-graph'); + await ownedGraphIds.seed({ size: ${scenario === 'late-create-response' ? 1 : 0} }); if (${JSON.stringify(scenario)} === 'setup-timeout') await new Promise(() => {}); await use(true); }, { timeout: 100 }], @@ -48,10 +55,10 @@ test('intentional negative control', async ({ setup }) => {}); const report = JSON.parse(result.stdout); assert.equal(report.stats.unexpected, 1, result.stdout); assert.equal(report.stats.skipped, 0); - assert.deepEqual(JSON.parse(readFileSync(events, 'utf8')), scenario === 'setup-timeout' - ? ['unroute', 'deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('] - : ['unroute', 'deleteEdges']); + assert.deepEqual(JSON.parse(readFileSync(events, 'utf8')), scenario !== 'cleanup-failure' + ? ['createRequested', 'created', 'unroute', 'deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('] + : ['createRequested', 'created', 'unroute', 'deleteEdges']); const errors = report.suites[0].specs[0].tests[0].results[0].errors.map(error => error.message).join('\n'); - assert.match(errors, scenario === 'setup-timeout' ? /Fixture "setup" timeout/ : /Owned graph cleanup failed/); + assert.match(errors, scenario !== 'cleanup-failure' ? /Fixture "setup" timeout/ : /Owned graph cleanup failed/); }); } diff --git a/tests/lib/seedGraph.ts b/tests/lib/seedGraph.ts index 938c8e91..9ed0529a 100644 --- a/tests/lib/seedGraph.ts +++ b/tests/lib/seedGraph.ts @@ -61,13 +61,17 @@ export interface SeedOptions { batchSize?: number; /** Register ownership before node/edge setup, so partial setup can be cleaned. */ onGraphCreated?: (graphId: string) => void; + /** Stop future writes, while draining an in-flight create for its owned ID. */ + signal?: AbortSignal; } export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise { const { size, edgeFactor = 1.4, spacing = 130, namePrefix = 'Scale' } = opts; + opts.signal?.throwIfAborted(); const me = await gql(page, '{ me { id } }'); const userId = me.me.id; + opts.signal?.throwIfAborted(); const g = await gql( page, `mutation($input: [GraphCreateInput!]!) { createGraphs(input: $input) { graphs { id } } }`, @@ -102,6 +106,7 @@ export async function seedLargeGraph(page: Page, opts: SeedOptions): Promise Date: Tue, 27 Oct 2026 12:17:18 -0700 Subject: [PATCH 12/14] Record forbidden seed writes in timeout regression ledger --- tests/lib/runner/owned-graph-fixture.test.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/lib/runner/owned-graph-fixture.test.mjs b/tests/lib/runner/owned-graph-fixture.test.mjs index 198ed048..0871728c 100644 --- a/tests/lib/runner/owned-graph-fixture.test.mjs +++ b/tests/lib/runner/owned-graph-fixture.test.mjs @@ -30,6 +30,10 @@ const fakePage = { calls.push('created'); return { createGraphs: { graphs: [{ id: 'owned-graph' }] } }; } + if (query.includes('createWorkItems') || query.includes('createEdges')) { + calls.push('unexpected-post-timeout-write'); + throw new Error('Seeding wrote after setup timed out'); + } if (variables.id !== 'owned-graph') throw new Error('Unexpected graph ownership'); const operation = ['deleteEdges', 'deleteWorkItems', 'deleteGraphs', 'graphs('].find(name => query.includes(name)); calls.push(operation); From 2158f357b92fb5f549f9d42f0d2499dd69928adb Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 12:55:41 -0700 Subject: [PATCH 13/14] Preserve explicit camera intent during initial graph framing --- ...xplicit-camera-input-must-stop-auto-fit.md | 43 ++++++++++++++ .agents/issues/INDEX.md | 1 + .../InteractiveGraphVisualization.tsx | 8 ++- tests/e2e/graph/semantic-lod.spec.ts | 56 +++++++++++-------- 4 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 .agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md diff --git a/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md b/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md new file mode 100644 index 00000000..86cb724e --- /dev/null +++ b/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md @@ -0,0 +1,43 @@ +# Explicit camera input must stop pending automatic framing + +On 2026-10-27, [PR #192 CI run 40567753567](https://github.com/GraphDone/GraphDone-Core/actions/runs/40567753567) +tested feature head `74c122d17897d61272e056df02a7957a686f2e2a` on GitHub's merge +composition `f7dd01a57766ff1919237c9852cabfd5130d6eac`. Fixture setup no longer +exhausted the interaction timeout, but both semantic LOD cases failed their +first requested camera scale: expected 0.5, observed 0.1379447032 at 1440px +and 0.0720887246 at 390px. The basic smoke sequence passed 6/6; new-user had +42 passes, two LOD failures and the harness's failed-process evidence row. +This is a distinct application race, not the earlier fixture-budget failure. + +## Cause and corrective rule + +Initial auto-fit samples the settling layout every 250ms. Canvas pointer/wheel +input stopped that loop, but the minimap's explicit camera functions only set +`cameraManuallyPositionedRef`. Because the minimap is outside the canvas, a +pending fit could overwrite its requested zoom. Overview inspection had the +same omission. Explicit pan, zoom and item inspection now mark the current +graph as framed; direct D3 source events do so too. Programmatic automatic +fit/restore still does not claim manual intent. The existing graph ID guard +keeps this ownership scoped to the current graph. + +The first warm local negative control passed, showing that ordinary timing +was insufficient to reproduce the CI race reliably. The browser regression +now pauses the browser clock before graph loading, advances through the first +fit at 350ms, requests the minimap zoom, then advances six 250ms intervals +while checking the camera remains at 0.5. It resumes real time for the remaining +mouse/touch/navigation and persistence checks. It does not extend timeouts, +add retries, wait for auto-fit to finish, or bypass the production camera path. + +With the strengthened test, restoring the unmodified component from +`74c122d` caused the desktop case to fail at the camera-stability assertion; +restoring the fix passed the same case. Both ordinary desktop/phone cases +also passed before clock control. Commands used Node 22.18.0, an owned +Neo4j/API/Vite stack, and headed Playwright with one worker and zero retries. +Final two-viewport, related camera/resize/detail and CI results are recorded in +the PR discussion; this lesson alone does not claim those pending gates pass. + +Limits: these checks cover camera intent and immutable stored positions on the +301-item fixture. They do not establish full multilevel semantic projection, +1k/10k performance, hosted behavior or deployed-source identity. Preserve the +original failed CI artifacts and the warm negative-control pass alongside the +deterministic failure; a later green run must not erase either observation. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 9327d61b..447c341d 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| Minimap zoom or overview inspection is overwritten while a newly loaded graph settles | [Explicit camera input must stop automatic framing](../inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md); timer-controlled `semantic-lod.spec.ts` negative control | | Dense-graph browser case exhausts its timeout after slow API fixture creation; cleanup errors disappear | [Bound fixture setup separately and fail on cleanup errors](../inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md); `tests/e2e/graph/semantic-lod.spec.ts` | | GitHub connector can read a repo but PR creation returns `Resource not accessible by integration` | [Client-specific write permissions](../inbox/2026-10-27-github-connector-write-permissions.md); verify existing authorized access without changing credentials or policy | | LOD inspection moves the camera but loses selection; hidden source controls or nested SVG definitions leak across renderers | [Display projections need explicit renderer ownership](../inbox/2026-10-27-lod-projections-need-renderer-ownership.md); `semantic-lod.spec.ts`, `semanticLOD.test.ts` | diff --git a/packages/web/src/components/InteractiveGraphVisualization.tsx b/packages/web/src/components/InteractiveGraphVisualization.tsx index 49c7be7b..86722b0e 100644 --- a/packages/web/src/components/InteractiveGraphVisualization.tsx +++ b/packages/web/src/components/InteractiveGraphVisualization.tsx @@ -4127,7 +4127,10 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i // D3 sets sourceEvent only for direct wheel/pointer gestures. Programmatic // fit/restore transforms must remain distinguishable so an automatic fit // that later clips a restored task can be repaired on reload. - if (event.sourceEvent) cameraManuallyPositionedRef.current = true; + if (event.sourceEvent) { + cameraManuallyPositionedRef.current = true; + if (currentGraphIdRef.current) fittedGraphsRef.current.add(currentGraphIdRef.current); + } g.attr('transform', event.transform); setCurrentTransform({ x: event.transform.x, @@ -4294,6 +4297,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i setSelectedNodes(new Set([id])); setEditingEdge(null); cameraManuallyPositionedRef.current = true; + fittedGraphsRef.current.add(currentGraphIdRef.current); const k = 1.2; const transform = d3.zoomIdentity.translate(containerRef.current.clientWidth / 2 - item.x * k, containerRef.current.clientHeight / 2 - item.y * k).scale(k); d3.select(svgRef.current).interrupt().call(zoomBehaviorRef.current.transform, transform); @@ -4489,6 +4493,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i (window as any).miniMapNavigate = (graphX: number, graphY: number) => { if (!svgRef.current || !containerRef.current || !zoomBehaviorRef.current) return; cameraManuallyPositionedRef.current = true; + if (currentGraphIdRef.current) fittedGraphsRef.current.add(currentGraphIdRef.current); const svg = d3.select(svgRef.current); const k = d3.zoomTransform(svgRef.current).k || 1; const w = containerRef.current.clientWidth; @@ -4502,6 +4507,7 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i (window as any).miniMapZoom = (graphX: number, graphY: number, targetK: number) => { if (!svgRef.current || !containerRef.current || !zoomBehaviorRef.current) return; cameraManuallyPositionedRef.current = true; + if (currentGraphIdRef.current) fittedGraphsRef.current.add(currentGraphIdRef.current); const svg = d3.select(svgRef.current); const k = Math.max(0.1, Math.min(4, targetK)); const w = containerRef.current.clientWidth; diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index df2c3413..f0f8534c 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -127,10 +127,13 @@ for (const width of [1440, 390]) localStorage.setItem('currentGraphId', id); localStorage.setItem('graphdone:viewMode', 'graph'); }, seeded.graphId); + await page.clock.install(); + await page.clock.pauseAt(new Date(Date.now() + 100)); await page.goto('/'); await expect(page.locator('.graph-container .node')).toHaveCount(301, { timeout: 30000, }); + await page.clock.runFor(350); const overview = page.locator('.semantic-overview'); await expect(overview).toHaveAttribute('data-active', 'true'); const cards = page.locator('[data-lod-group]'); @@ -139,6 +142,37 @@ for (const width of [1440, 390]) element.setAttribute('data-lod-identity', 'retained'); }); expect(await cards.count()).toBeGreaterThan(0); + const longTitleItem = before.find( + (item: { id: string }) => item.id === seeded.nodeIds[8] + ); + const zoomToTitle = async (scale: number, advanceClock = false) => { + await page.evaluate( + ({ x, y, scale }) => + ( + window as unknown as { + miniMapZoom: (x: number, y: number, k: number) => void; + } + ).miniMapZoom(x, y, scale), + { x: longTitleItem.positionX, y: longTitleItem.positionY, scale } + ); + if (advanceClock) await page.clock.runFor(150); + await expect + .poll(() => + svg.evaluate( + element => + (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k + ) + ) + .toBeCloseTo(scale); + }; + await zoomToTitle(0.5, true); + for (let sample = 0; sample < 6; sample++) { + await page.clock.runFor(250); + expect(await svg.evaluate(element => + (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k + )).toBeCloseTo(0.5); + } + await page.clock.resume(); const geometry = await cards.evaluateAll(elements => elements.map(element => { const rect = element.getBoundingClientRect(); @@ -174,28 +208,6 @@ for (const width of [1440, 390]) await page.screenshot({ path: `test-artifacts/quality/semantic-lod-${width}.png`, }); - const longTitleItem = before.find( - (item: { id: string }) => item.id === seeded.nodeIds[8] - ); - const zoomToTitle = async (scale: number) => { - await page.evaluate( - ({ x, y, scale }) => - ( - window as unknown as { - miniMapZoom: (x: number, y: number, k: number) => void; - } - ).miniMapZoom(x, y, scale), - { x: longTitleItem.positionX, y: longTitleItem.positionY, scale } - ); - await expect - .poll(() => - svg.evaluate( - element => - (element as SVGSVGElement & { __zoom: { k: number } }).__zoom.k - ) - ) - .toBeCloseTo(scale); - }; for (const scale of [0.5, 0.9, 0.99, 1.1]) { await zoomToTitle(scale); await expect(overview).toHaveAttribute('data-active', 'true'); From 9709196ed3c6eb69b63c5bf3d5b3d382ca61f04a Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 12:58:57 -0700 Subject: [PATCH 14/14] Retain initial-fit legibility coverage in camera regression --- ...xplicit-camera-input-must-stop-auto-fit.md | 6 +++ tests/e2e/graph/semantic-lod.spec.ts | 52 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md b/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md index 86cb724e..1af569ef 100644 --- a/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md +++ b/.agents/inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md @@ -41,3 +41,9 @@ Limits: these checks cover camera intent and immutable stored positions on the 1k/10k performance, hosted behavior or deployed-source identity. Preserve the original failed CI artifacts and the warm negative-control pass alongside the deterministic failure; a later green run must not erase either observation. + +Independent review identified a lost initial-fit geometry sample after moving +zoom earlier. The test now checks font size, viewport bounds and card overlap +both at the first automatic fit and after explicit zoom, retaining both samples +in its geometry attachment. Clock control keeps the first sample from waiting +out the pending fit loop. diff --git a/tests/e2e/graph/semantic-lod.spec.ts b/tests/e2e/graph/semantic-lod.spec.ts index f0f8534c..d0f88249 100644 --- a/tests/e2e/graph/semantic-lod.spec.ts +++ b/tests/e2e/graph/semantic-lod.spec.ts @@ -142,6 +142,33 @@ for (const width of [1440, 390]) element.setAttribute('data-lod-identity', 'retained'); }); expect(await cards.count()).toBeGreaterThan(0); + const overviewGeometry = async () => { + const geometry = await cards.evaluateAll(elements => + elements.map(element => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + right: rect.right, + bottom: rect.bottom, + font: parseFloat( + getComputedStyle(element.querySelector('span')!).fontSize + ), + }; + }) + ); + for (const a of geometry) { + expect(a.font).toBeGreaterThanOrEqual(14); + expect(a.x).toBeGreaterThanOrEqual(0); + expect(a.right).toBeLessThanOrEqual(width); + for (const b of geometry.filter(b => b !== a)) + expect( + a.x < b.right && a.right > b.x && a.y < b.bottom && a.bottom > b.y + ).toBe(false); + } + return geometry; + }; + const initialGeometry = await overviewGeometry(); const longTitleItem = before.find( (item: { id: string }) => item.id === seeded.nodeIds[8] ); @@ -173,34 +200,13 @@ for (const width of [1440, 390]) )).toBeCloseTo(0.5); } await page.clock.resume(); - const geometry = await cards.evaluateAll(elements => - elements.map(element => { - const rect = element.getBoundingClientRect(); - return { - x: rect.x, - y: rect.y, - right: rect.right, - bottom: rect.bottom, - font: parseFloat( - getComputedStyle(element.querySelector('span')!).fontSize - ), - }; - }) - ); - for (const a of geometry) { - expect(a.font).toBeGreaterThanOrEqual(14); - expect(a.x).toBeGreaterThanOrEqual(0); - expect(a.right).toBeLessThanOrEqual(width); - for (const b of geometry.filter(b => b !== a)) - expect( - a.x < b.right && a.right > b.x && a.y < b.bottom && a.bottom > b.y - ).toBe(false); - } + const geometry = await overviewGeometry(); await info.attach('overview-geometry.json', { body: JSON.stringify({ width, nodes: before.length, edges: seeded.edgeCount, + initialGeometry, geometry, }), contentType: 'application/json',