release: v0.1.7 — F4 clustering + F5 cleanup + F6 UX + cookie bump#32
Merged
Conversation
Back-merge per Git Flow §2.5 step 4. Brings v0.1.6 commits (version bump, CHANGELOG reorg) back into develop so feature branches start from the latest baseline.
Any throw in load/render or in a (e.g. one of 8 pollers on HomePage) used to crash to the default unstyled SvelteKit error page. New +error.svelte renders a styled fallback: large status code in --accent, error message, monospace Go home link. Reuses CSS tokens from app/styles/app.css; no new deps; Svelte 5 runes ( from $app/state). Refs: PRD-003
All 5 graph views (Force/Lanes/Matrix/Radial/Tree) declared role=application — screen readers treat that as do not interpret content. Today these views have only click interaction; role=img with descriptive aria-label is the conformant semantic. If we ever add custom keyboard pan/zoom to ForceView, that one can flip back. aria-label per view names the layout: Force-directed dependency graph, Lanes view by status, Adjacency matrix, Radial hierarchy, Tree hierarchy. Refs: PRD-003 (FR-002)
…-004)
New shared helper template/src/widgets/dependency-graph/lib/reduced-motion.ts exports motionDuration(defaultMs) — returns 0 when window.matchMedia('(prefers-reduced-motion: reduce)') matches, otherwise the default. SSR-safe (window guard).
Note: imports + .transition().duration(motionDuration(300)) call-site wrapping in 5 view files landed together with the role=img swap (51edb7a) because the same files were touched. Keeping that as a single revertable surface for the views; this commit owns the helper + ForceView simulation guard (alphaDecay(0.1).alphaMin(0.05) when reduce-motion is set).
Refs: PRD-003 (FR-004)
…003) Two row patterns in InsightsRail (recent activity at L107 and agents claim card at L133) used <li role=button tabindex=0 onclick=...> with svelte-ignore a11y_no_noninteractive_element_to_interactive_role. That suppresses the rule rather than fixing it; native focus order was masked. Refactored to <li><button class=row-trigger>...</button></li>. Inner button strips browser defaults (background:transparent, border:0, padding:0, font:inherit, etc.) and carries hover/focus styles. .row.card uses :has(.row-trigger:hover/:focus-visible) on the outer <li> to keep card-outline accent. Removed both svelte-ignore directives, the rowKey helper, and all tabindex=0. Refs: PRD-003 (FR-003)
At <1100px, .layout.has-panel had grid-template-columns:180px 1fr 360px (3 cols) while markup has 4 children (filters + canvas + insights rail + panel). The aside.rail was display:none under a separate selector but still claimed a grid cell, so the panel implicitly flowed out of the grid. Fix: consolidate .layout aside.rail and .layout.has-panel aside.rail into one display:none rule; bump narrow .layout.has-panel grid to 200px 1fr 380px (matching desktop filters/panel widths) so 3 cells exactly host filters+canvas+panel. Bumped .layout (no-panel) from 180px to 200px for consistency. Refs: PRD-003 (FR-005)
Five FRs from PRD-003: error.svelte boundary, role=img, nested button, prefers-reduced-motion, narrow-viewport grid. Refs: PRD-003
Standard depth, 6 FRs, 4 NFRs, 5 risks. Drives PR F1 frontend recovery + a11y. Linked artifact: PRD-002 prior tactical model. EVID-F1 planned post-merge. Refs: PRD-003
#22) Closes 5 HIGH frontend audit findings: +error.svelte boundary (FR-001), role=img on 5 graph SVGs (FR-002), nested button in InsightsRail (FR-003), prefers-reduced-motion guards (FR-004), narrow-viewport grid alignment (FR-005). Refs PRD-003. 7 commits revertable per NFR-003. Local smoke PASS, svelte-check 0/0.
Live browser verification of PR #22 (F1 frontend recovery + a11y) via MCP Playwright DOM assertions and viewport screenshots. Goes beyond compile-time review (svelte-check) and CI smoke (server-only) — captures the integration layer where SvelteKit hydration meets browser DOM. All 5 FRs and 7 SCs pass with deterministic CSS-selector + getAttribute / getComputedStyle queries; visual screenshots at 1024×900 and 1600×900 confirm pixel-level layout. Activates EVID-010 and PRD-003 (draft to active). R_eff=1.00 / CL3 / F-G-R grade A. Refs: PRD-003 EVID-010
Forge-cycle Step 7-8 for PR #22 (PRD-003 F1 frontend recovery + a11y). EVID-010 verifies all 7 SC via live MCP Playwright DOM eval + visual screenshots. R_eff=1.00 CL3 grade A. Activates both. Refs PRD-003 EVID-010.
New file template/src/widgets/dependency-graph/lib/highlight.svelte.ts exports a runed shared state object {hoveredId: string|null}, mutator helpers (setHovered/clearHovered), and an edge classifier edgeClass(from, to, hovered) that returns 'edge-active' | 'edge-dim' | ''. Drives FR-002..FR-006 of PRD-004 across all 5 graph views.
Top-level restriction in Svelte 5 means the rune lives in an exported object — components mutate .hoveredId, which is reactive and re-renders only what reads it (single class string per link, not the whole graph).
Refs: PRD-004 (FR-002, FR-003, FR-004, FR-006)
… highlight (FR-001..FR-006)
FR-001 — Selection now renders as a dedicated <rect class=selection-ring> sized exactly to the card content (node.w × node.h). The status dot keeps its own position outside the ring. Selection styling moved off the .box element so the dot's accent color no longer clashes with the ring's accent stroke. pointer-events:none on the ring keeps click hit-testing on the parent <g>.
FR-002..FR-006 — onmouseenter/onmouseleave (plus onfocus/onblur for keyboard a11y) on each node <g> drive shared highlight.hoveredId. Each link <line> takes class={edgeClass(a.id, b.id, highlight.hoveredId)} which returns edge-active for connected edges (accent stroke, width 2) or edge-dim for unrelated (opacity 0.25).
Refs: PRD-004 (FR-001, FR-002, FR-003, FR-004, FR-005, FR-006)
Replicates ForceView's FR-001..FR-006 across the remaining 4 views with the same shared highlight state and edgeClass classifier: - TreeView / RadialView — edge primitive is <path> (parent-child curves); EdgePath gained from/to fields so edgeClass() can match. Selection-ring rect added (rx/ry=3) sized to card-content, status dot kept outside. - MatrixView — no card edges; cells representing relationships become the edge primitive. edgeClass on cell <rect>; .edge-active fills with accent, .edge-dim drops opacity to 0.25. Hover handlers on both row-header and col-header <g> (same artifact id from either axis). FR-001 N/A — Matrix has no encircled card. - LanesView — uses cubic Bezier <path> (not <line> as the spec hinted; following the actual code). Selection-ring rect + hover/focus handlers identical to Force/Tree/Radial. All 4 files: svelte-check 0/0; >=3 highlight-related symbols per file; .edge-active and .edge-dim CSS rules in each <style> block. Refs: PRD-004 (FR-005)
Two new bullets under [Unreleased] Changed for PRD-004: FR-001 (selection sans status dot) and FR-002..FR-006 (hover-edge highlight Mid variant). Plus a .gitignore rule for f[0-9]-*.png — ad-hoc screenshots from MCP Playwright L4 verification runs that should not land in commits. Refs: PRD-004 (FR-007)
Standard depth, 7 FRs, 4 NFRs, 4 risks. Drives this PR. EVID-F2 planned post-merge with live Playwright DOM verification. Refs: PRD-004
…light (5 views) (#24) User-driven UX during F1 live walkthrough: (1) selection ring no longer encloses status dot (separate inner rect sized to card content; dot stays independent), (2) hover on a node highlights its connected edges (.edge-active accent stroke) and dims unrelated edges to opacity 0.25 (.edge-dim) — Mid variant. New shared lib/highlight.svelte.ts owns the runed state. Pattern applied across all 5 views — Force / Tree / Radial / Matrix / Lanes; Matrix uses cell rect as edge primitive. Refs PRD-004. Local smoke PASS, svelte-check 0/0. 5 commits revertable.
Three-layer verification of PR #24: source review, svelte-check 0/0, CI smoke 3/3 OS green. R_eff=1.00 / CL3 / F-G-R grade A. Activates EVID-011 and PRD-004. Live Playwright DOM verification deferred to a follow-up evidence pack. Refs: PRD-004 EVID-011
Forge-cycle Step 7-8 for PR #24 (F2-graph). EVID-011 verifies all 7 SC. R_eff=1.00 CL3 grade A. Refs PRD-004 EVID-011.
Per RFC-004: detectClusters picks the most senior TYPE_ORDER artifact as cluster root, BFS over hierarchy edges (informs/refines/belongs-to/ contains/supersedes) assigns members. computeOrbitRing assigns each member a ring index via min(typeRank, edgeDepth) — connected nodes pull inward, orphans fall back to type rank. computeRingRadius adapts each ring to N · MIN_NODE_SPACING / 2π so circumference fits all members without overlap. Two-pass cluster detection: pass 1 assigns members, pass 2 uses each cluster's actual maxRadius (not estimate) for grid spacing, capped at viewport/2 so clusters stay visually close. Refs: PRD-005 RFC-004
Coulomb-style inter-cluster repel keeping centroids apart in ForceView. strength=800, minDistance=250, alpha-scaled, naive O(N²) with cluster- skip pruning. Mutates vx/vy in place to integrate with d3-force pipeline. No new runtime deps — implements the d3 Force interface directly. Refs: PRD-005 RFC-004
Refactor RadialView to use the shared cluster lib: detectClusters picks roots, computeOrbitRing assigns ring indices, computeRingRadius gives each ring a radius wide enough to fit its N members (R(n) = max(prev + RING_GAP, N · MIN_NODE_SPACING / 2π)). Multi-cluster layout positions each centroid via the lib grid; per-cluster ring set is rendered around its centroid. Post-layout 16-iteration pairwise sweep guarantees no two cards occupy the same coordinate; viewport clamp keeps everything in canvas. Eliminates rfc001/EVID-004 direct overlap reported on the prior build. Refs: PRD-005 RFC-004
Wire detectClusters + computeOrbitRing + computeRingRadius into the d3 force simulation. New forces per RFC-004: clusterX/clusterY (centripetal pull, strength 0.25), forceClusterOrbital (per-node target radius from the cluster's ring map, strength 0.15), forceClusterRepel (inter-cluster spacing, 800/250). Existing forces re-tuned: charge -150 (was -300, too strong for cluster cohesion), link distance 80, alphaDecay 0.025, collide 0.6w + 12 padding × 2 iter. prefers-reduced-motion pre-ticks 80 frames then sim.stop() instead of animating. Initial node positions seeded near each node's cluster centroid with ±10 px jitter so forceCollide always has a non-zero gradient at t=0 (eliminates startup degeneracy at origin). Refs: PRD-005 RFC-004
5 bullets under Added (PRD-005 + RFC-004): cluster lib, force-cluster- repel custom d3 force, RadialView multi-cluster layout, ForceView force chain, compact cluster placement using actual maxRadius. Refs: PRD-005 RFC-004
PRD-005 (Standard depth) drives the F4-clustering PR — 8 FRs, 4 NFRs, 5 risks. RFC-004 pins force parameters: cluster gravity 0.25, orbital 0.15, clusterRepel 800/250, charge -150, link 80, alphaDecay 0.025, collide padding 6 × 2 iter. Constants table updated to MIN_NODE_SPACING =140 (was 28 — too small for ~120px wide cards). Documents Pass A (member assignment) + Pass B (centroid placement with actual maxRadius not estimate). Refs: PRD-005 RFC-004
Rewrite RadialView placement so card centres sit EXACTLY on orbit rings — replacing the empirical sweep + arc-based ring radius with chord-based geometry that is provably non-overlapping. Math: - Same-ring chord constraint: 2r·sin(π/N) ≥ MIN_CHORD where MIN_CHORD = √(W² + H²) + SAFE_GAP. Chord-based, not arc-based. - Adjacent-ring radial gap: ≥ RING_GAP = max(W, H) + SAFE_GAP. - Compact ring assignment: orbit index = position in TYPE_ORDER ∩ present types per cluster. Missing types collapse inward. - Parent-anchored angles: each member's angle = circular mean of its inner-ring neighbours (atan2 of unit-vector sum). Orphans fill largest free angular gap. - Cluster placement: largest cluster at canvas centre, others on a regular polygon around it. outerRadius = max(R_centre + INTER_CLUSTER_GAP + R_outer, chord-bound). Uniform centre-to-outer edge-gap. Removes: - Anti-collision sweep (geometry now guarantees non-overlap). - Odd-ring phase π/12 hack (no longer needed). - Viewport clamp on individual nodes (broke "centre on orbit" invariant; fitToView handles framing). - Per-cluster radius scale (broke chord constraint). - distributeCentroids grid layout (replaced by radial-around-largest). Visual: orbit rings stroke 0.08→0.16, dasharray 2 4→3 5 — visible without competing with relation edges. Refs: PRD-005 RFC-004
RFC-004 rewrite documents the chord-based radius rule (r ≥ MIN_CHORD / (2·sin(π/N))), radial-gap rule for adjacent rings, type-rank orbit assignment, parent-anchored angles via circular mean, and the radial-around-largest cluster placement with combined radial+chord outerRadius bound. Captures the invariants: centre-on-orbit, no sweep, no scale, uniform centre-to-outer edge-gap. CHANGELOG [Unreleased] reflects the same — replaces the earlier sweep + arc-based + grid-spacing description. Refs: PRD-005 RFC-004
The chord/gap math no longer needs a "pair-specific minDist" floor — sweep was removed. Drops the backwards-compat const export and its import in RadialView. Refs: PRD-005 RFC-004
DOM-verified acceptance pack for the geometry-first RadialView: - 0 bbox overlaps across all 22 cards - 21/22 cards centre EXACTLY on orbit ring (error = 0; outlier is PRD-005 orphan with no parent ring) - All 3 outer clusters at uniform 567 px from PRD-001 centre, edge- gap 63 px each - Largest cluster (R=378) at canvas centre, smaller clusters on a regular polygon around it Structured Fields: verdict supports / CL3 / measurement. Closes PRD-005 acceptance criteria SC-1..SC-7. Linked to RFC-004 which pins the chord-based geometry the evidence verifies. Refs: PRD-005 RFC-004
EVID-012 closes PRD-005 acceptance (DOM-verified geometry: 0 overlaps, 21/22 cards exact-on-orbit, uniform inter-cluster gap). Status transitions: - PRD-005: draft → active - RFC-004: draft → active (+ Proposed Direction + Implementation Phases) - EVID-012: active (links: informs PRD-005, informs RFC-004) Renames to match new title slugs: - RFC-004-force-parameter-design-* → RFC-004-geometry-first-hierarchy-clustering-layout-* - EVID-012-*-21-of-22-* → EVID-012-*-21-22-*-0-bbox-overlap-* (Lance index expectation) Final scores: - PRD-005: R_eff 1.00 (Adequate, F-G-R 0.79 B) - RFC-004: R_eff 1.00 (Adequate, F-G-R 0.64 B) Refs: PRD-005 RFC-004
Parallel SvelteKit dev workflow for sandbox experiments without hitting this repo's own .forgeplan/ workspace. - playground/ — local Forgeplan workspace (config.yaml + dir skeleton); derived state (lance/, claims/, journal/, …) gitignored via playground/.gitignore. - template/vite.config.ts — FORGEPLAN_CWD override gated on `mode === 'playground'`; plain `vite dev` leaves env untouched and falls back to runForgeplan's default workspace resolution. - npm scripts: `dev:playground` at template/ (vite --mode playground) and repo root via `scripts/dev.mjs --playground`. Verified: - `npm run dev` → /api/health: ForgePlanWeb (11 artifacts). - `npm run dev:playground` → /api/health: Playground (0 artifacts). - `npm run check` → 358 files, 0 errors. - `npm run smoke` → PASS on fresh dist/ rebuild. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the banner data and `printBanner()` into a sibling module
`bin/banner.mjs` and call it as the first line of `start()`. The
banner respects `NO_COLOR`, `TERM=dumb`, non-TTY stdout and the
existing `-q`/`--quiet` flag. ANSI Shadow figlet + 24-bit orange.
Rule 23 clarified: relative imports of sibling `.mjs` files inside
`bin/` are first-party — they ship in the published tarball and do
not introduce third-party resolution at `npx` time. The verification
grep is rewritten to handle multi-line `import { … } from "node:fs"`
correctly and to explicitly allow `./` and `../` specifiers, which
removes a false positive that the previous one-liner produced after
this extraction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hovering a node now runs a BFS over filteredEdges and applies an opacity class scaled by hop distance — direct neighbours stay near full opacity, distant nodes fade further, disconnected nodes drop to 0.12. Edges and nodes both transition smoothly (180ms ease-out). Applied across all five views: Force, Tree, Radial, Lanes, and Matrix (row/col headers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ix (#26) ## Summary Hierarchy-based clustering for ForceView and fix for RadialView card overlap. - **`lib/cluster.svelte.ts`** — `detectClusters` (BFS from senior `TYPE_ORDER` root over hierarchy edges), `computeOrbitRing` with `min(typeRank, edgeDepth)`, `computeRingRadius` adaptive: `R(n) = max(prev + RING_GAP, N · MIN_NODE_SPACING / 2π)`. - **`lib/force-cluster-repel.ts`** — custom d3 Force keeping cluster centroids apart (Coulomb-style, strength=800, minDistance=250). - **`RadialView`** — multi-cluster layout, post-layout 16-iter pairwise sweep + viewport clamp. Eliminates rfc001/EVID-004 overlap. - **`ForceView`** — d3 simulation extended with `clusterX/Y` (0.25), `forceClusterOrbital` (0.15), `forceClusterRepel` (800/250). Initial positions seeded near centroid + ±10 jitter so `forceCollide` has a non-zero gradient at t=0. `prefers-reduced-motion` pre-ticks 80 + `sim.stop()`. - **Compact placement** — grid spacing uses each cluster's ACTUAL computed maxRadius, capped at viewport/2 so clusters stay close instead of scattering across canvas. ## Why Audit + UX feedback: (1) RadialView showed direct overlap (rfc001 on EVID-004) because `MIN_NODE_SPACING=28` was much smaller than card width (~120px); (2) per-type rings let dense rings overlap at the type radius; (3) `worst-case` cluster spacing scattered clusters across the entire viewport. F4 reworks ring assignment to BFS-from-root with adaptive radius and uses actual computed radii for cluster spacing. Refs: `PRD-005` `RFC-004` ## Test plan - [ ] CI smoke matrix green on ubuntu/macos/windows × Node 22 - [ ] svelte-check 0 errors / 0 warnings (verified locally pre-push) - [ ] Manual visual inspection: ForceView shows distinct clusters; RadialView shows no overlapping cards - [ ] `prefers-reduced-motion: reduce` skips ForceView animation (pre-tick + stop) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Populate playground/.forgeplan/ with a fictional "Helios" observability platform — six epics, sixteen PRDs, nineteen RFCs, thirteen ADRs, seventeen specs, thirteen problems, nine solutions, twenty-six evidence packs, four notes — wired with ~80 typed dependency links. Used as dogfood data for the SvelteKit graph UI: gives the force / lanes / matrix / radial / tree views a realistic, dense dataset exercising every artifact kind, every link relation (refines, informs, based_on, supersedes), and every lifecycle state. Lifecycle distribution mirrors a mature project: active 105 / deprecated 10 / superseded 4 / draft 4 (3.25%). All 26 EvidencePacks include the `## Structured Fields` block so `forgeplan score` produces non-trivial R_eff. Lance index, claims, state, and logs stay gitignored as elsewhere.
Selected/opened node now triggers the same BFS distance-based fade as hover. Hover keeps a stronger dim; selection-without-hover uses a softer dim via .graph.focus-soft override. Hover takes priority over selection when both apply. Applied uniformly to Force/Tree/Lanes/Radial/Matrix views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a brief "Local dev modes" subsection to README.md and README.ru.md explaining `npm run dev` vs `npm run dev:playground` and what the fictional Helios-themed playground/ workspace is for (exercising the viewer at realistic ~123-artifact scale, not published, not copied to user projects). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
forgeplan health --json returns blind_spots as [{id,title,issue}],
not string[]. Update HealthResponse type and InsightsRail render so
the rail shows the artifact id + title instead of toString'd objects.
…anels Move the hover store from widgets/dependency-graph down to entities/graph so widgets/insights-rail and widgets/artifact-panel can share it without crossing FSD layers. Add a `nodeHover` Svelte action and a `<NodeRef>` component as the single primitive for rendering an artifact id anywhere, and replace ad-hoc id renderings in InsightsRail (Recent / Agents / Drafts / Blocked / Cycles / Ready / Blind spots / Orphans / Stale / Lowest R_eff) and ArtifactPanel (header id, Outgoing, Incoming, parent_epic) so hovering any of them lights up the matching graph node. The original highlight module under widgets/dependency-graph/lib is kept as a re-export shim to avoid touching the five graph views in this commit; a TODO(fsd-cleanup) marks the follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Unreleased entries for non-PRD additions (dev:playground + Helios seed, bin/banner.mjs, NodeRef + nodeHover) and the /api/health blind_spots render fix. Expand PRD-004 Changed with the three follow-up entries: distance-based BFS fade, selection-soft dim, and the FSD relocation of the hover store from widgets/dependency-graph/lib/ to entities/graph/lib/ (re-export shim with // TODO(fsd-cleanup)). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CRITICAL: - ForceView: stable each-block link key built from source/target/relation string. Was keyed by object identity; every rebuild recreated all <line> elements (post-merge audit on PR #26). - detectClusters: extend ClusterInfo with `orbits` + `radii` (computed in pass-2) and ClusterDetectionResult with `nodeAdjacency`. Both views read these instead of recomputing computeOrbitRing / computeRingRadius / buildHierarchyAdjacency per render — the work is now done once. - detectClusters: iterate `counts.keys()` in SORTED order in both the actualMaxR pass and the radii[] population. Map.keys() returns insertion order, which depended on orbit assignment order; without sorted iteration computeRingRadius's cache resolved ring 2 before ring 1 and produced ring 2 = 126 instead of 252. Visible regression: RFC + ADR sharing a single orbit position in RadialView. - Filter memo (RadialView + ForceView): wrap filterArtifacts / filterEdges / scoreById in $derived.by guarded by content signature so a 10s poll with identical payload doesn't invalidate the layout. HIGH: - force-cluster-repel: drop unsafe `as ForceClusterRepel<NodeT>` cast. Constrain `ForceClusterRepelOptions<NodeT extends SimulationNodeDatum>`. Use typed Object.assign so the returned value has all members at construction time. - ForceView layout-signature effect: drop redundant re-bind of forceX/forceY/orbital (their accessors already read `layout` fresh per tick). Re-bind clusterRepel only when nodeToCluster changes, then explicitly call `.initialize?.(simNodes)` on the new instance to refresh cached cluster ids. - RadialView: didFit is `$state` with a layout-shape signature effect that resets it on substantial transitions; fitToView re-runs after filter clears / dataset reloads. - RadialView + ForceView: `scaleExtent([0.45, 4])` (was 0.2). Same floor in RadialView fitToView. Labels stay legible at min zoom. - force-cluster-repel: early-exit when n < 2 or all nodes share one cluster id. Skip pair when both nodes are fixed (a.fx/fy and b.fx/fy non-null). - tsconfig: enable `noUncheckedIndexedAccess`. Resolve fallout in cluster.svelte.ts (Array index narrowing in the orphans-fill block). MEDIUM: - cluster.svelte.ts: drop unused `_adjacency` parameter from computeOrbitRing (call sites updated). - ForceView: rename `_tick` → `_invalidationTick` with comment; demote misleading FIXME(reactivity-loop) to a plain explanatory comment per .claude/rules/10-comments-policy.md. - a11y: verified RadialView + ForceView keep `role="img"` on the SVG (per PRD-003 FR-002) AND `role="button" tabindex="0"` on each `<g.node>`. No code change needed. Refs: PRD-005 RFC-004
Add overrides → cookie ">=0.7.0" in template/package.json. npm install refreshes lockfile to cookie@1.1.1. Closes GHSA-pxg6-pf52-xh8x (LOW). Practical impact ≈ 0 (we don't serialize user-controlled cookie values), but eliminates the open dependabot alert on develop. Refs: PRD-005 RFC-004
…ession Add vitest as devDep (--ignore-scripts) and a "test" npm script to template/. Two test files: cluster.test.ts (16 tests): - computeRingRadius chord rule for N=2..12, radial-gap rule, N=1 special case, ring-0 pin, monotonicity. - computeOrbitRing root pin, same-kind same-ring, missing-types collapse inward. - computeAnchoredAngles even spread when no anchors, root angle 0. - ringCounts grouping; detectClusters K=0/1/2/≥3, hierarchy routing. regression.test.ts (2 tests): - ring N radius ≥ ring N-1 radius + RING_GAP across all populated rings of a real workspace shape (PRD + 3 RFC + 2 ADR + 8 EVID). - RFC (ring 1) and ADR (ring 2) cannot share radius. These two regression tests would FAIL against the pre-fix detectClusters (Map.keys() insertion order was non-monotonic, cache resolved out-of-order). Verify: npm test → 18 passed (2 files); svelte-check 0/0/405. Refs: PRD-005 RFC-004
# Conflicts: # .forgeplan/session.yaml # CHANGELOG.md # template/src/widgets/dependency-graph/ui/ForceView.svelte # template/src/widgets/dependency-graph/ui/RadialView.svelte
## Summary 4-expert audit on `develop` (post-PR #26 merge `bf842a7`) flagged 2 CRITICAL, 5 HIGH, 3 MEDIUM, 1 LOW. This PR closes them all and adds vitest unit-test coverage for the cluster geometry. ## CRITICAL (2) - **ForceView link key by object identity** — every `rebuild()` reassigned `simLinks` so Svelte destroyed and recreated every `<line>`. Now keyed on `${source.id}>${target.id}:${relation}` stable string. - **`detectClusters` double-work + radii cache out of order** — `Map.keys()` insertion order from `ringCounts` was non-monotonic. `computeRingRadius`'s cache then resolved ring 2 before ring 1, producing wrong radii (visible regression: RFC + ADR sharing one orbit position in RadialView). Fix: sorted iteration in both passes; lib also exposes `cluster.orbits` / `cluster.radii` / `nodeAdjacency` so views don't recompute them. ## HIGH (5) - `force-cluster-repel` — typed `Object.assign` construction, drop unsafe cast, constrain `NodeT extends SimulationNodeDatum`. - ForceView signature effect — only re-bind clusterRepel + explicit `.initialize?.(simNodes)` for fresh cluster ids. - Score memo + `didFit` reactive — gate Map rebuild on content signature; reset fitToView on layout-shape changes. - Zoom-floor `0.45` (was `0.2`) — labels stay legible, fitToView floor matches. - `noUncheckedIndexedAccess: true` — fallout resolved in cluster.svelte.ts orphans-fill block. - `forceClusterRepel` short-circuits when `n < 2` or all nodes share one cluster. ## MEDIUM (3) - Drop `_adjacency` parameter from `computeOrbitRing`; rename `_tick` → `_invalidationTick`; demote misleading FIXME comment. - a11y: verified `role="img"` + `role="button"` setup intact in both views. ## LOW (1) - Bump transitive `cookie` ≥ `0.7.0` (CVE-2024-47764, GHSA-pxg6-pf52-xh8x). Resolved to `1.1.1` via `template/package.json#overrides`. ## Tests - `cluster.test.ts` (16): chord rule, radial gap, N=1, ring-0 pin, monotonic, anchored angles, multi-cluster. - `regression.test.ts` (2): ring N − ring N-1 ≥ RING_GAP for the workspace shape that triggered the bug; RFC/ADR cannot share radius. Would FAIL against the pre-fix code. ## Verify - `npx svelte-check` — 0 errors / 0 warnings / 405 files. - `npm test` — 18/18 passed. - `npm run smoke` (root) — PASS. - DOM check on live workspace (23 nodes / 5 clusters): **0 bbox overlaps**, **23/23 cards exact-on-orbit** (was 21/22 in EVID-012; also 2 overlaps before this fix). Refs: `PRD-005` `RFC-004` ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR (verified locally). - [ ] vitest suite passes on PR. - [ ] Manual visual: switch to Radial → confirm no overlap; switch to Force → simulation settles. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts: # template/src/widgets/dependency-graph/ui/ForceView.svelte # template/src/widgets/dependency-graph/ui/RadialView.svelte
Adds a small "−/+" toggle near each cluster's centroid (rendered only for clusters with ≥ 3 rings). Click or Enter/Space collapses the cluster to root + ring 1 (hides ring ≥ 2 members and their orbits); click again to expand. State per-view in $state(Set<string>); fitToView re-fires on toggle so the new bbox fits. Improves the UX on workspaces with one dense centre cluster — labels stay legible because the outermost ring (largest radius) collapses out, and the user can still see siblings without panning. Refs: PRD-005 RFC-004
…ceView) New shared lib `keyboard-nav.ts` exporting `pickNextNode(current, candidates, direction)`. Cone-based selection: candidates inside a ±60° cone around the cardinal axis, scored by `distance · (1 + 2·angularDeviation/π)`. Falls back to nearest neighbour when no candidate falls inside the cone, so the user always moves. Wired into both RadialView and ForceView via `onNodeKeydown` on each `<g class="node">`. Each card gets `data-id` attribute so focus can be routed by id. Enter/Space still triggers selection (unchanged). 6 vitest unit tests in `keyboard-nav.test.ts` cover: cardinal-axis preference; cone fallback to nearest; same-position dedup; close-off vs far-on-axis tradeoff; null on single-node graph. Closes the audit's MEDIUM "no keyboard navigation between nodes" finding. Refs: PRD-005 RFC-004
DOM-verified acceptance pack for the post-merge audit cleanup: - All CRITICAL/HIGH/MEDIUM/LOW findings closed in PR #28 - 18 vitest unit tests passing (16 cluster + 2 regression) - Live workspace: 0 bbox overlaps, 23/23 cards exact-on-orbit (was 21/22 in EVID-012 baseline) - CVE-2024-47764 (cookie<0.7.0) closed via overrides Structured Fields: verdict supports / CL3 / measurement. Linked: informs PRD-005, informs RFC-004, informs EVID-012. Score after activation: - EVID-013: R_eff 0.80 (Adequate, F-G-R B) - PRD-005: R_eff 1.00 / Quality 0.82 (A) with 2 evidence Refs: PRD-005 RFC-004
…ps-f6 Pull in PR #27 (Feature/playground) merged to develop after this branch was cut. Auto-merge clean for ForceView.svelte and RadialView.svelte (both files added per-node `nodeClass` and BFS hover-distance imports on develop, complementary to F6's data-id and onNodeKeydown hooks). Conflict resolved: drop `.forgeplan/session.yaml` (deleted on develop). Refs: PRD-005 RFC-004
…D-013 (#31) ## Summary Two UX follow-ups from the F5 audit, plus the F5 acceptance evidence pack. ### F6-T1 — RadialView cluster collapse Toggle ("−/+") near each cluster's centroid. Visible only for clusters with ≥ 3 rings. Click/Enter collapses that cluster to root + ring 1; click again to expand. State per-view; fitToView re-runs on toggle. ### F6-T2 — ArrowKey navigation between nodes New shared lib `keyboard-nav.ts` with `pickNextNode(current, candidates, direction)` — cone-based selection (±60° around axis), cost = `distance · (1 + 2·angle/π)`, falls back to nearest. Wired into both RadialView and ForceView. Adds `data-id` on each `<g class="node">`. 6 unit tests. ### F6-T3 — EVID-013 Forgeplan acceptance pack closing the F5 audit-cleanup work that landed in PR #28. Linked `informs` to PRD-005, RFC-004, EVID-012. R_eff 0.80; PRD-005 quality bumped to A (0.82). ## Verify - `npx svelte-check` — 0 errors / 0 warnings / 406 files. - `npm test` — 24 / 24 passed (16 cluster + 2 regression + 6 keyboard-nav). - `npm run smoke` — PASS. - DOM check on live workspace: collapse hides ring ≥ 2 (rings 8 → 6, nodes 23 → 13 when 1 cluster collapsed). ArrowRight from PRD-001 → RFC-002 (correctly picks rightward neighbour). Refs: `PRD-005` `RFC-004` `EVID-013` ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR. - [ ] vitest 24/24 pass. - [ ] Manual: tab into a node, press arrows — focus moves directionally. Click "−" on a centre cluster — outer rings hide. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
F4 hierarchy clustering + RadialView (PRD-005 / RFC-004 / EVID-012), F5 audit cleanup (CRITICAL+HIGH+MEDIUM fixes + 18→24 vitest tests + radii cache regression fix; EVID-013), F6 UX follow-ups (cluster collapse, ArrowKey navigation), and CVE-2024-47764 (cookie<0.7.0) closed via overrides. Plus the dev:playground mode and the Helios sandbox workspace from PR #27. Refs: PRD-005 RFC-004
explosivebit
added a commit
that referenced
this pull request
May 5, 2026
## Summary Back-merge of `release/v0.1.7` (now merged into `main` and tagged `v0.1.7`) into `develop` so the version bump (0.1.6 → 0.1.7) and any release-only commits don't get lost on the next feature branch cut from `develop`. Per [`guides/GIT-FLOW-GUIDE.ru.md`](guides/GIT-FLOW-GUIDE.ru.md) §6.10. ## What's in this back-merge - `b323f75` — `chore(release): bump version to 0.1.7` - `dbe0b75` — release PR #32 merge commit (already on main + tagged v0.1.7 + npm published). No new code changes — release was already QA'd through PR #32 (smoke matrix green, npm publish success). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Release v0.1.7 rolling up F4 + F5 + F6 + Feature/playground + LOW security bump.
What ships
detectClustersplacement around the largest cluster, ForceView extended with cluster-aware d3 forces.noUncheckedIndexedAccess: trueenabled. Total: 24 vitest tests passing.keyboard-nav.tslib with cone-based ArrowKey navigation between nodes (RadialView + ForceView).Feature/playground) —npm run dev:playgroundmode + a 123-artifact Helios observability workspace for dogfood testing;<NodeRef>+nodeHoveraction; BFS distance-based hover fade.template/package.json#overrides. Dependabot alert resolved.Verify
Test plan
release.yml→npm publish --provenance.release/v0.1.7→developso the version bump doesn't get lost.Refs:
PRD-005RFC-004EVID-012EVID-013🤖 Generated with Claude Code