release: v0.1.8 — F11+F12+F13+F14+hotfix#42
Merged
Conversation
## 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)
Adds the Sankey graph view alongside Force/Tree/Radial/Matrix/Lanes:
- `lib/sankey-layout.ts` — `assignSankeyColumns` (BFS-based depth from
any hierarchy root; cycles fall back to column 0) and
`buildSankeyPayload` (d3-sankey-shaped {nodes, links}; pre-set
`column` + filtered to forward-only edges).
- `lib/sankey-layout.test.ts` — 6 unit tests covering linear chains,
multiple parents (min depth wins), non-hierarchy edge filtering,
cycle handling, payload shape.
- `ui/SankeyView.svelte` — d3-sankey driven layout with horizontal
links (`sankeyLinkHorizontal`), zoom/pan, fitToView at 0.45 floor,
hover-highlight + BFS distance fade reusing the shared lib,
selection ring on the node bar, status-dot + kind colour.
- `lib/relation.ts` — new `relationStroke` per relation kind for
Sankey link colours (informs / refines / belongs-to / contains /
supersedes / risks).
- DependencyGraph router: case `view === 'sankey'`.
- `shared/config/ui-prefs.ts` GraphView extended with `sankey`.
Adds dependencies: `d3-sankey ^0.12.3`, `@types/d3-sankey ^0.12.4`
(installed with --ignore-scripts).
Refs: PRD-005
Adds the Sunburst graph view as the 7th mode: - `lib/sunburst-layout.ts` — `buildSunburstTree` (synthetic workspace root + first-parent attach; cycles broken by visited set; ignores non-hierarchy edges) and `computeSunburstPartition` wrapping `d3-hierarchy.partition` over the tree. - `lib/sunburst-layout.test.ts` — 7 unit tests covering disconnected artifacts at root, first-parent attach, cycle handling, partition bounds (x in [0, 2π], y in [0, radius]), synthetic-root depth. - `ui/SunburstView.svelte` — d3-shape `arc()` per sector, label rotation that flips beyond π for readability, hover/select via shared highlight lib, zoom/pan, kind-coloured fill. - DependencyGraph router: case `view === 'sunburst'`. - `shared/config/ui-prefs.ts` GraphView extended with `sunburst`. Adds dependencies: `d3-hierarchy ^3.1.2`, `d3-shape ^3.2.0`, `d3-scale ^4.0.2` and matching `@types/*`. Refs: PRD-005
…elect
Replaces the BFS-depth columns / first-parent attach in the F7-T2/T3
draft with a type-tier-driven layout that uses the same TYPE_ORDER
hierarchy as the cluster lib (epic → prd → spec → rfc → adr →
evidence → ...).
New shared lib `lib/type-tier.ts`:
- `typeTier(kind)` returns position in TYPE_ORDER.
- `compactTierMap(kinds)` builds the per-workspace mapping with
missing tiers collapsed inward (so PRD/RFC/EVID workspace gets
{prd: 0, rfc: 1, evidence: 2} — no empty `spec` row).
- `normaliseHierarchyEdge(from, to, relation)` rotates each
hierarchy relation to abstract→concrete form so the layout always
flows from parent (lower tier) to child (higher tier):
contains → source=parent, target=child
belongs-to → target=parent (X belongs to Y → Y parent)
refines → target=parent (RFC refines PRD → PRD parent)
informs → target=parent (EVID informs PRD → PRD parent)
supersedes → source remains; intra-tier, dropped from layout.
Sankey rewrite (`lib/sankey-layout.ts` + `ui/SankeyView.svelte`):
- column = compact tier of kind; layer-gap 160 logical units between
adjacent type-tier columns ⇒ visually distinct "slices" instead
of one wall of bars.
- intra-tier edges (e.g. supersedes between two ADRs) dropped; all
remaining links flow strictly left→right after normalisation.
- per-column tier headers (PRD / RFC / ADR / EVIDENCE) and dashed
vertical dividers — the user reads taxonomy at a glance.
- nodeSort: most-connected nodes float to the top of their column,
then alphabetical id tiebreak.
- bar/label states: idle = dim; hover/focus = full opacity, accent
stroke + drop-shadow, label → white; selected = accent stroke,
label → accent. Persistent visual lock on the picked artifact.
Sunburst rewrite (`lib/sunburst-layout.ts` + `ui/SunburstView.svelte`):
- tree depth = compact-tier index. Each artifact attaches to its
highest-tier (closest from below) hierarchy parent so the rings
of the sunburst line up with the type taxonomy:
inner ring = most-abstract type present (PRD here)
next ring = RFC / ADR
outer ring = EVIDENCE
- ancestor-chain highlight: hovering a sector lights its full
ancestor path, dims the rest of the chart. White text on the
active sector; ancestor labels stay readable.
- selection mirrors the focus styling but persists.
Global focus reset (`app/styles/app.css`):
- `svg g[role="button"]:focus { outline: none; }` removes the
Chromium/Safari blue/white default focus ring on graph nodes
across all views (keyboard nav still works via `tabindex="0"`,
the highlight is replaced by our accent ring).
Tests: `sankey-layout.test.ts` and `sunburst-layout.test.ts` rewritten
to assert direction normalisation (informs/refines/contains all
produce parent → child links) and tier-based ring assignment.
Verify: svelte-check 0/0/423; npm test 40/40 (16 cluster + 2 regression
+ 6 keyboard-nav + 9 sankey + 7 sunburst); smoke PASS.
Refs: PRD-005
…ast hover/select) (#34) ## Summary Adds two new graph view modes alongside Force/Tree/Radial/Matrix/Lanes, both driven by the same TYPE_ORDER hierarchy as the cluster lib so all hierarchical views read consistently. ### Sankey - Columns = compact tier of artifact kind (epic / prd / spec / rfc / adr / evidence). Missing tiers collapse inward. - Layer gap 160 logical units between columns ⇒ visually distinct tiers, not a wall of bars. - Per-column tier header text + dashed vertical divider. - Edge direction normalised abstract → concrete via `normaliseHierarchyEdge` (so `informs` / `refines` / `belongs-to` flow target → source while `contains` keeps source → target). - nodeSort: most-connected first, alphabetical tiebreak. - Hover/focus → bar accent stroke + glow, label white; selected → accent stroke + accent label (persistent). ### Sunburst - Concentric rings = compact tier (inner = abstract / outer = concrete). - Each artifact attaches to its highest-tier hierarchy parent. - Hover lights the full ancestor chain; the rest of the chart dims to 0.18 fill-opacity. - Selected mirrors focus styling, persistent across mouse-leave. ### Global UX - Removed the Chromium/Safari default blue/white focus ring on `svg g[role="button"]` across all views — replaced by our accent stroke + drop-shadow. Keyboard nav unaffected. ### Shared lib `type-tier.ts` - `typeTier(kind)`, `compactTierMap(kinds)`, `normaliseHierarchyEdge(from, to, relation)` — used by both Sankey and Sunburst lib + ready for future tier-aware views. ### Deps Added: `d3-sankey`, `d3-hierarchy`, `d3-shape`, `d3-scale` (+ `@types/*`), installed with `--ignore-scripts`. ## Verify - `npx svelte-check` — 0/0/423. - `npm test` — 40/40 (cluster 16 + regression 2 + keyboard-nav 6 + sankey 9 + sunburst 7). - `npm run smoke` — PASS. - DOM check on the live workspace (Helios playground / 24 artifacts): - Sankey shows 4 tier columns (PRD / RFC / ADR / EVIDENCE) with visible 50+px gaps. - Sunburst inner ring = PRDs, middle = RFCs/ADRs, outer = EVIDs. - Hover/select gives white/accent text + accent stroke; ancestor chain lights up in Sunburst. Refs: PRD-005 ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR. - [ ] vitest 40/40 pass. - [ ] Manual: tab into a node, click — selection persists with accent label. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5-expert audit on develop @ 60823f1 found 3 CRITICAL, 12 HIGH, 16 MEDIUM, 18 LOW. This PR closes all of them across the 7 graph views. CRITICAL (3): - Selection-ring color: ForceView/TreeView/RadialView/LanesView all drew the .selection-ring with stroke=kindBorder(node.kind), which is dim grey for note/evidence kinds → selection invisible after mouse-leave. Now hardcoded `stroke: var(--accent)`. - didFit one-shot: SankeyView + SunburstView never reset didFit. Filter clears reduced sectors/columns but zoom transform stayed, rendering microscopic. Ported the layout-shape signature pattern from RadialView; both views now refit on substantial structural change. HIGH (12): - Dropped dead `d3-scale` + `@types/d3-scale` (~50 KB never imported). - New `lib/filter-memo.svelte.ts` exports `nodesContentSignature` / `edgesContentSignature`. Applied to Tree/Matrix/Lanes/Sankey/Sunburst (Force/Radial already inline). 10s polls with identical payload no longer cascade-invalidate. - Sankey tier-header contrast: `var(--fg-2)` + `font-weight: 500` (was 0.55 white opacity, failed WCAG AA at zoom-out). - Sunburst label gate is zoom-aware: `> 40 / max(0.5, transform.k)` AND hide rings ≥ 4 when k < 0.5. Labels stop overlapping on outer rings. - Per-view :focus-visible accent stroke verified in 5 views; Matrix uses .row-label/.col-label fill + .cell stroke variant. - TS: new `isLinkResolved<N>(l)` type guard in lib/d3.ts; new `getOrInit<K,V>(map, k, factory)` in lib/map-utils.ts. Drops 2 `as Node` casts in ForceView and 4 lazy-init bucket sites in cluster.svelte.ts. SankeyLink type cleanly Omit-intersected with SankeyPayloadLink. - LOW security: `CSS.escape(id)` wraps the data-id selector in ForceView and RadialView focusNodeById — defense-in-depth even though /api/get validates ids. MEDIUM (F10): - `.node.selected .label { fill: var(--accent); }` verified in all 4 box-based views. - RadialView nodesSig now suffixes `|c:${collapsedSig}` so the cluster-collapse toggle invalidates the filtered-nodes memo deterministically. - Matrix: `.cell.is-row, .cell.is-col` accent stroke when the selected node is the row/col index — selection bleeds through to the cell tint, not just the row/col header strip. - Sankey fitToView ceiling 1 → 1.5 so narrow workspaces fit larger. - ArrowKey nav wired into TreeView and LanesView (mirrors Force/ Radial pattern). pickNextNode lib was already generic. - HomePage canvas-hint caption (11px var(--font-mono)) renders the active view's `hint` from GRAPH_VIEWS — disambiguates the hover- fade semantic between BFS-distance views (5) and Sunburst's ancestor-chain view. LOW: - Sunburst sector aria-label appended with `(ring ${depth}, parent ${parentId})` — screen readers now hear the chain context. - `@media (prefers-reduced-motion: reduce) { svg *, svg *::before, svg *::after { transition: none !important; animation: none !important; } }` in app.css — covers all hover-fade/zoom CSS transitions across the 7 views. Verify: - svelte-check 0 errors / 0 warnings / 425 files. - npm test 40 / 40 pass. - npm run smoke PASS. Refs: PRD-005
…m 5-expert audit (#35) ## Summary 5-expert multi-agent audit on develop @ 60823f1 (TS / Frontend / Security / Performance / UX) found 3 CRITICAL, 12 HIGH, 16 MEDIUM, 18 LOW. This PR closes them all in one pass across the 7 graph views. ## CRITICAL - **Selection-ring colour** in 4 views (Force/Tree/Radial/Lanes) was drawn with `stroke=kindBorder(node.kind)` — dim grey for note/evidence kinds, so selected node was invisible after mouse-leave. Hardcoded `stroke: var(--accent)`. - **`didFit` one-shot** in SankeyView + SunburstView: filter clears shrunk the layout but zoom transform stayed, rendering microscopic. Ported the layout-shape signature reset pattern from RadialView. ## HIGH - Dropped dead `d3-scale` + `@types/d3-scale` (~50 KB never imported). - New `lib/filter-memo.svelte.ts` exports content-signature helpers; applied to Tree/Matrix/Lanes/Sankey/Sunburst — 10s polls with identical payload no longer cascade-invalidate the layout pipeline. - Sankey tier-header contrast → `var(--fg-2)` + `font-weight: 500` (WCAG AA). - Sunburst label gate is zoom-aware (`> 40 / max(0.5, transform.k)` + hide rings ≥4 when k<0.5). - Per-view `:focus-visible` accent stroke verified in 5 views. - TS `isLinkResolved<N>(l)` type guard + `getOrInit<K,V>` helper. Drops `as Node` and 4 lazy-init bucket sites. - LOW security: `CSS.escape(id)` wraps `data-id` selector in focusNodeById. ## MEDIUM (F10) - `.node.selected .label { fill: var(--accent); }` across 4 box-based views. - RadialView `nodesSig` includes `collapsedSig` so cluster-collapse toggle invalidates deterministically. - Matrix `.cell.is-row, .cell.is-col` accent stroke for cells in the selected row/column. - Sankey fitToView ceiling `1 → 1.5`. - ArrowKey navigation wired into TreeView + LanesView. - HomePage `.canvas-hint` caption renders the active view's `hint` — disambiguates Sunburst's ancestor-chain fade vs the rest's BFS-distance fade. ## LOW - Sunburst sector aria-label appended with `(ring N, parent ID)`. - Global `@media (prefers-reduced-motion: reduce) { svg *::* { transition: none } }` in `app.css`. ## Verify - `npx svelte-check` — 0 errors / 0 warnings / 425 files. - `npm test` — 40 / 40 pass. - `npm run smoke` — PASS. ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean. - [ ] vitest 40/40. - [ ] Manual: select a node in Force/Tree/Radial/Lanes — accent ring + accent label persist after mouse-leave. - [ ] Filter clear in Sankey/Sunburst — fitToView re-fits to the new bbox. - [ ] Tab/Arrow keys move focus directionally in Tree/Lanes (was Force/Radial only). Refs: PRD-005 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements PRD-006 / RFC-005. Two coupled UX improvements that turn @forgeplan/web from a "table of contents" into a "decision viewer". Body preview: - ArtifactPanel adds "+ Show body / − Hide body" toggle revealing the full markdown body of the selected artifact inline. - New `widgets/artifact-panel/lib/markdown-renderer.ts` uses marked (GFM tables / code / checkboxes) + DOMPurify (allow-listed tags, no scripts, no javascript: URLs). - 6 unit tests cover basic markdown, XSS strip, GFM tables, task checkboxes, empty-string fallback. - bodyExpanded state persisted per session in localStorage. Decision impact drill-down: - "Show downstream" / "Show upstream" buttons in ArtifactPanel. - New `widgets/dependency-graph/lib/impact-graph.ts` exports computeDownstream / computeUpstream pure functions, BFS bounded by MAX_IMPACT_DEPTH=8, direction normalised via type-tier.ts#normaliseHierarchyEdge so all hierarchy relations (informs / refines / belongs-to / contains / supersedes) flow abstract → concrete consistently. - 7 unit tests cover linear chain, diamond, cycle, depth cap, non-hierarchy filter, upstream chain, downstream/upstream symmetry. - highlight.svelte.ts extended with impactRoot / impactDirection $state fields; setImpactRoot() and impactedClass() exported. Dual-exposed via entities/graph and widgets/dependency-graph/lib for FSD layering. - 5 views (Force / Tree / Radial / Lanes / Matrix) wire impact map via $derived.by; outer SVG gets `impact-mode` class; nodes get `node-impact-root` / `node-impacted-near` / `node-impacted-far` applied alongside existing `nodeClass(...)`. Sankey + Sunburst do NOT participate — their hierarchy semantics already cover the impact view. - Matrix applies impact class to row + col headers (artifacts); cells (which represent edges) are skipped. Visual treatment in app.css: - node-impact-root: stroke var(--accent), stroke-width 2, drop-shadow(0 0 8px var(--accent)). - node-impacted-near: opacity 1. - node-impacted-far: opacity 0.7. - impact-mode unaffected nodes: opacity 0.18. Dependencies: - runtime: marked@^18, dompurify@^3. - dev: @types/dompurify, happy-dom (vitest DOM env for renderer test). Verify: - svelte-check 0 errors / 0 warnings / 432 files. - npm test 53/53 (40 baseline + 13 new — 6 renderer, 7 impact-graph). - npm run smoke PASS. Refs: PRD-006 RFC-005
PRD-006 (Standard) — Web body preview + decision impact drill-down. 9 SC, 8 FR, 5 NFR, 5 risks. Targets code reviewer / stakeholder / new team member personas. Bundle ≤ 80 KB tracked via NFR-001. DOMPurify + CSP layered for XSS (NFR-003). RFC-005 (Standard) — Markdown rendering + decision impact algorithm. Pins marked + DOMPurify config, BFS depth cap (MAX_IMPACT_DEPTH=8), direction normalisation via type-tier.ts#normaliseHierarchyEdge. Sankey + Sunburst explicitly excluded from impact mode. PRD-007 (Standard) — Web stale + blind-spot push notifications. 9 SC, 9 FR, 5 NFR, 5 risks. Browser Notification API only (no SW). Privacy: only id + title in payload (NFR-002). Throttle 1 per 60s per category. F12 implementation. RFC-006 (Standard) — Notification permission UX + breach detection. State machine for default / requested / granted / denied. Breach categories: new blind_spot / stale_count delta / orphan_count delta. notifyBus pattern for cross-scope click-handler routing. All 4 validated: PRD-006/007 PASS (with non-blocking warnings), RFC-005/006 PASS clean. Refs: PRD-006 RFC-005 PRD-007 RFC-006
…FC-005) (#36) ## Summary Implements PRD-006 + RFC-005. Two coupled UX improvements that turn `@forgeplan/web` from a "table of contents" into a "decision viewer": ### Body preview - ArtifactPanel adds **+ Show body / − Hide body** toggle revealing the full markdown body inline. - New `widgets/artifact-panel/lib/markdown-renderer.ts` uses `marked` (GFM) + DOMPurify (allow-listed tags). XSS-safe by construction. - 6 unit tests (basic markdown, `<script>` strip, `javascript:` href strip, GFM table preserved, task checkbox `<input type="checkbox">`, empty fallback). - bodyExpanded persisted per session in `localStorage`. ### Decision impact drill-down - **Show downstream / Show upstream / Clear** buttons in ArtifactPanel. - New `widgets/dependency-graph/lib/impact-graph.ts` — pure BFS bounded by `MAX_IMPACT_DEPTH=8`, direction normalised via `type-tier.ts#normaliseHierarchyEdge` so `informs` / `refines` / `belongs-to` / `contains` / `supersedes` all flow abstract → concrete consistently. - 7 unit tests (chain, diamond, cycle, depth cap, non-hierarchy filter, upstream chain, symmetry). - `highlight.svelte.ts` extended with `impactRoot` / `impactDirection` $state + `setImpactRoot` / `impactedClass` exports. Dual-exposed via `entities/graph` and `widgets/dependency-graph/lib`. - 5 views (Force / Tree / Radial / Lanes / Matrix) wire impact map via `$derived.by`; outer SVG gets `class:impact-mode`; nodes get `node-impact-root` / `node-impacted-near` / `node-impacted-far`. - Sankey + Sunburst do NOT participate — their hierarchy semantics already cover this view. - Matrix applies impact class to row + col headers; cells (edges) skipped. ### Visual treatment - `node-impact-root`: accent stroke 2px + drop-shadow. - `node-impacted-near`: opacity 1. - `node-impacted-far`: opacity 0.7. - `impact-mode` unaffected nodes: opacity 0.18. ### Dependencies - runtime: `marked@^18`, `dompurify@^3`. - dev: `@types/dompurify`, `happy-dom` (vitest DOM env). ## Verify - svelte-check 0/0/432 files. - npm test 53/53 (40 baseline + 13 new). - npm run smoke PASS. ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean. - [ ] vitest 53/53. - [ ] Manual: open ArtifactPanel for a PRD, click "+ Show body" — markdown renders. Click "Show downstream" — affected RFCs/EVIDs glow accent in graph. Refs: `PRD-006` `RFC-005` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements PRD-007 + RFC-006. Browser Notification API (no Service
Worker) fires on workspace health changes detected by the 10s
/api/health poll diff.
Detection (entities/health/lib/notify.svelte.ts):
- detectBreaches(prev, next) returns 3 categories:
- new blind_spot — id appeared in next.blind_spots that wasn't in prev
- stale — next.stale_count > prev.staleCount
- orphan — next.orphan_count > prev.orphanCount
- All pure functions; 9 vitest unit tests cover snapshot extraction,
every breach category, permission branches (granted/denied/missing),
throttle window with injectable now() clock.
Permission UX (HealthBar.svelte):
- 🔔/🔕 toggle button with active state when granted+opted-in.
- First click → requestPermission(); on grant, sets notify=true.
- denied → button disabled with tooltip "Re-enable in browser site
settings".
- Feature-detect: hidden when Notification API absent (NFR-003).
- Hidden aria-live="polite" .sr-only mirror for screen-readers.
Wiring (HomePage.svelte):
- One $effect watches healthPoller.state.data — diffs against
prevHealthSnapshot, runs detectBreaches, fires per-breach
notification with onClick → focusArtifact(id).
- Second $effect watches notifyBus.pendingFocus — when set,
selectNode({id}) and clears. Bus pattern lets notification's
onclick (foreign closure) reach Svelte 5 state without prop drill.
Settings (settings.ts):
- notify: boolean added to PersistedSettings + ResolvedSettings.
- Default false. Loaded/saved through existing localStorage flow.
Throttle:
- ≥ 60s per category (FR-007).
- silent: true on Notification ctor (no audio).
- tag: `forgeplan.${kind}` collapses repeats in the OS notification
centre.
- No body content in payload — only id + title for blind_spots,
count delta for stale/orphan (NFR-002 privacy).
Vitest config:
- pool: 'threads' (was default 'forks'). Default per-file child
process spawning blew through macOS kern.maxprocperuid at 8
test files; threads share heap, no spawn() per file.
Verify:
- svelte-check 0/0/434.
- npm test 62/62 (53 baseline + 9 new).
- node scripts/smoke.mjs PASS.
Refs: PRD-007 RFC-006
EVID-014 (Tactical, supports / CL3 / test) — DOM-verified F11 acceptance: PR #36 merged to develop, 13 new vitest tests, 3-OS smoke matrix green. SC-1..SC-6, SC-8, SC-9 all pass; SC-7 (bundle delta) tracked but pending per-bundle measurement. Linked: informs PRD-006, informs RFC-005, builds-on EVID-013. Status transitions: - PRD-006: draft → active - RFC-005: draft → active These activations ride along with the F12 PR for atomic delivery. F11 code already shipped in PR #36; this commit only updates artifact metadata and adds the evidence pack. Refs: PRD-006 RFC-005 EVID-014
…06) (#37) ## Summary Implements PRD-007 + RFC-006. Browser Notification API (no Service Worker) fires when the 10s `/api/health` poll detects workspace decay. Plus EVID-014 closes F11 and activates PRD-006 + RFC-005. ### Detection - `entities/health/lib/notify.svelte.ts` exports pure helpers: `snapshotFromHealth` / `detectBreaches` / `notificationsSupported` / `notificationPermission` / `requestPermission` / `fire`. - 3 breach categories: new blind_spot, stale_count delta, orphan_count delta. - Throttle ≥ 60s per category with injectable `now()` for tests. - 9 unit tests cover snapshot extraction, all 3 breach categories, permission branches (granted/denied/missing), throttle window. ### Permission UX - HealthBar 🔔 / 🔕 toggle. Active state when granted + opted in. - First click → `requestPermission()`; grant flips toggle on. - Denied → button disabled with explanatory tooltip. - Hidden when `'Notification' in window === false` (Firefox no-API users, SSR). ### Click → focus - Notification's `onclick` writes to a `notifyBus.pendingFocus` $state singleton. - HomePage's `$effect` reads it and calls `selectNode({id})`. Cross-scope state without prop drill. ### Privacy & a11y - No body content in payload (NFR-002): only id + title for blind_spots, delta count for stale/orphan. - `silent: true` on the Notification (no audio). - Hidden `aria-live="polite"` `.sr-only` mirror in HealthBar — screen readers announce regardless of OS notification visibility. ### Settings - `notify: boolean` field added; persisted via existing `loadSettings` / `saveSettings` localStorage flow. ### Vitest config - `pool: 'threads'` (was default `forks`). Default per-file child process spawning blew through macOS `kern.maxprocperuid` at 8 test files; threads share heap, no `spawn()` per file. Fix landed during build. ### EVID-014 / activations - EVID-014 (Tactical, supports / CL3 / test) closes F11 acceptance. - PRD-006 + RFC-005 transition draft → active. ## Verify - `npx svelte-check` — 0/0/434 files. - `npm test` — 62/62 (53 baseline + 9 new). - `node scripts/smoke.mjs` — PASS. ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR. - [ ] vitest 62/62. - [ ] Manual: toggle Notify on; trigger a stale by `forgeplan mark-stale <id>` in the workspace; observe browser notification; click it; tab focuses + artifact panel opens. Refs: `PRD-006` `RFC-005` `EVID-014` `PRD-007` `RFC-006` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
EVID-015 (Tactical, supports / CL3 / test) — F12 acceptance: PR #37 merged to develop, 9 new vitest tests (53 → 62), 3-OS smoke matrix green. SC-1..SC-9 all pass: - toggle UI + permission flow + 3 breach categories detected - localStorage persistence + UI state reflection - click → focus via notifyBus singleton - svelte-check 0/0; CI matrix 3-OS green Status transitions: - PRD-007: draft → active - RFC-006: draft → active These artifacts ride along with F13 (markdown export, tactical) for a single PR. F12 code already shipped in PR #37; this commit only updates artifact metadata. Refs: PRD-007 RFC-006 EVID-015
ArtifactPanel adds a "📋 Copy as markdown" button next to the impact
actions. Click writes a copy-paste-ready markdown summary to the
clipboard:
# {id} — {title}
**Status**: {status} · **Kind**: {kind} · **R_eff**: {r_eff|n/a}
## Outgoing
- {to} ({relation})
## Incoming
- {from} ({relation})
## Body excerpt
{first 500 chars + ...}
Outgoing / Incoming sections omitted when empty; Body section omitted
when artifact.body is empty. Closes the loop "PR review needs PRD
context" — paste straight into GitHub PR descriptions / Slack threads.
Visual feedback: ✓ Copied (green) on success, ✗ Copy failed (red) on
error. Both auto-revert after 2s. setTimeout cancelled on rapid clicks.
Pure function `buildMarkdownSummary` in widgets/artifact-panel/lib/
markdown-export.ts. 6 vitest unit tests (basic format, empty
outgoing/incoming, list rendering, R_eff n/a fallback, body
truncation past 500 chars, empty body omission).
Tactical — no PRD/RFC artifact required.
Verify:
- svelte-check 0/0/436.
- npm test 68/68 (62 baseline + 6 new).
Refs: tactical
…te F12 (#38) ## Summary Two riding-along payloads: ### F13 (tactical, no PRD/RFC) — Copy as markdown ArtifactPanel adds a **📋 Copy as markdown** button. Click writes a copy-paste-ready markdown summary to the clipboard for PR descriptions / Slack threads: ``` # {id} — {title} **Status**: ... · **Kind**: ... · **R_eff**: ... ## Outgoing - {to} ({relation}) ## Incoming - {from} ({relation}) ## Body excerpt {first 500 chars + ...} ``` Empty Outgoing/Incoming/Body sections omitted. Visual feedback: ✓ Copied (green) on success, ✗ Copy failed (red) on error; auto-revert after 2 s. Pure helper `buildMarkdownSummary` in `widgets/artifact-panel/lib/markdown-export.ts`. 6 vitest unit tests. ### EVID-015 + activations - EVID-015 (Tactical, supports / CL3 / test) closes F12 acceptance. - PRD-007 + RFC-006 transition draft → active. ## Verify - `npx svelte-check` — 0/0/436. - `npm test` — 68/68 (62 baseline + 6 new). ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR. - [ ] vitest 68/68. - [ ] Manual: open ArtifactPanel for any PRD, click "📋 Copy as markdown", paste into editor — markdown matches format. Refs: `PRD-007` `RFC-006` `EVID-015` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…diff The F12 breach-detection effect read prevHealthSnapshot via detectBreaches() AND wrote to it (`prevHealthSnapshot = next`) in the same effect. Since prevHealthSnapshot was declared `$state(...)`, Svelte 5 (correctly) flagged the cycle as effect_update_depth_exceeded and stopped re-running the effect. prevHealthSnapshot is a "previous tick" memo — no template reads it, no other effect depends on it. Reactivity is unnecessary. Demoting it to a plain `let` keeps the value alive across effect re-runs through the component closure scope without making it a reactive signal. Verify: - svelte-check 0/0/436. - npm test 68/68 (no test impact — pure-function notify lib unchanged). - Manual: dev server loads with no console errors. Refs: PRD-007 RFC-006
…diff (#40) ## Summary Hotfix for runtime error reported by user after F12 merge: ``` Uncaught Svelte error: effect_update_depth_exceeded Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state. ``` ## Root cause `HomePage.svelte:130` $effect for breach detection: - **reads** `prevHealthSnapshot` via `detectBreaches(prevHealthSnapshot, next, ...)` - **writes** `prevHealthSnapshot = next` at the bottom Both on the same `$state(...)` signal → Svelte 5 (correctly) detects the cycle and abort-loops the effect. ## Fix `prevHealthSnapshot` is a "previous tick" memo — no template reads it, no other effect depends on it. Reactivity is unnecessary; the value lives across re-runs via the component's closure scope. Demoted to plain `let`. ## Verify - `npx svelte-check` — 0/0/436. - `npm test` — 68/68 (no test impact; pure-function notify lib unchanged). - Manual: dev server loads, browser console shows 0 errors after navigation. ## Why $state was wrong here In Svelte 5 runes, `$state` creates a reactive signal. Any effect that reads + writes the same signal triggers a cycle. For "memoized previous value" between successive runs of one effect, a plain `let` survives in closure scope without becoming reactive — that's the idiomatic pattern. Refs: `PRD-007` `RFC-006` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5-expert audit (TS / Frontend / Security / Performance / UX) on
F11+F12+F13 returned 0 CRITICAL, 7 HIGH, ~12 MEDIUM, ~10 LOW. This
PR closes all 7 HIGH + 7 MEDIUM/LOW.
HIGH:
- TS-H1: renderBody returns branded SafeHtml type (string &
{ readonly __safeHtml: unique symbol }). Untrusted strings cannot
reach {@html} via this signature.
- TS-H2: TODO(marked-types) comment near `as string` cast.
- FE-H1: Matrix impact-mode selector extended to include row-header
and col-header (was only .node — Matrix never faded).
- FE-H2: try/catch around Notification.permission and
Notification.requestPermission and new Notification ctor.
Sandboxed iframes no longer throw on access.
- UX-H1: artifact-body pre / pre code get max-width: 100%,
white-space: pre-wrap, word-break: break-word. Long shell lines
wrap instead of cascading horizontal scroll to panel.
- PERF-H1: marked + DOMPurify dynamic-imported only when user clicks
"+ Show body". First-paint −21 KB gzip.
MEDIUM/LOW:
- FE-M2: clipboard fallback for non-secure contexts (textarea +
document.execCommand('copy')).
- FE-M3: liveText prefixed with zero-width-space + liveSeq counter
so identical breach text re-announces in screen readers.
- FE-M4: notify-on first-fire-storm prevention — prime
prevHealthSnapshot when user flips notify on mid-session.
- FE-L1: prefers-reduced-motion media query covers all *, not just
svg *.
- UX-M1: tooltips on Show downstream / upstream buttons.
- UX-M3: .links ul capped at max-height: 30vh + overflow-y: auto.
- SEC-L1: DOMPurify afterSanitizeAttributes hook adds
rel="noopener noreferrer" to target="_blank" anchors (CWE-1022).
Tests: 68 → 70.
- markdown-renderer.test.ts: target=_blank gets rel=noopener.
- notify.test.ts: iframe-throw fallback returns 'unsupported'.
Verify:
- svelte-check 0/0/436.
- npm test 70/70.
- npm run smoke PASS.
- Playwright DOM check: 0 console errors.
Bundle: 64 KB raw / 21 KB gzip of marked + DOMPurify moved from
first-paint chunk to lazy chunk loaded on body-toggle click.
Refs: PRD-006 RFC-005 PRD-007 RFC-006
…#41) ## Summary 5-expert audit (TS / Frontend / Security / Performance / UX) on F11+F12+F13 returned **0 CRITICAL, 7 HIGH, ~12 MEDIUM, ~10 LOW**. This PR closes all **7 HIGH + 7 MEDIUM/LOW** findings. ## HIGH | ID | Finding | Fix | |---|---|---| | **TS-H1** | `renderBody` return type was plain `string` — caller could feed any string to `{@html}` | Branded `SafeHtml = string & { readonly __safeHtml: unique symbol }` | | **TS-H2** | `marked.parse(...) as string` cast valid only with `async: false` — silent breakage if option removed | `// TODO(marked-types)` comment + revisit on marked v19 | | **FE-H1** | Matrix impact-mode never faded — CSS selector targeted `.node` only; Matrix has `.row-header` / `.col-header` | Selector extended via `:is(.node, .row-header, .col-header)` | | **FE-H2** | `Notification.permission` access throws in sandboxed iframes / restrictive Safari → toggle effect crashed | `try/catch` in `notificationPermission`, `requestPermission`, `fire` | | **UX-H1** | Long code blocks pushed horizontal scrollbar to entire panel | `.artifact-body pre / pre code` get `max-width: 100%; white-space: pre-wrap; word-break: break-word` | | **PERF-H1** | marked + DOMPurify (~21 KB gzip) shipped on first paint despite body collapsed by default | Dynamic import inside `bodyExpanded` branch — first-paint bundle drops by 21 KB gzip | ## MEDIUM / LOW - **FE-M2** — clipboard fallback (textarea + `execCommand('copy')`) for http:// non-secure contexts. - **FE-M3** — `liveText` prefixed with zero-width-space + `liveSeq` counter so screen readers re-announce identical breach text. - **FE-M4** — Notify-on first-fire-storm: prime `prevHealthSnapshot` when user flips notify on mid-session. - **FE-L1** — `prefers-reduced-motion` query extended from `svg *` to `*, *::before, *::after`. - **UX-M1** — tooltips on Show downstream / upstream buttons. - **UX-M3** — `.links ul { max-height: 30vh; overflow-y: auto }`. - **SEC-L1** — DOMPurify `afterSanitizeAttributes` hook forces `rel="noopener noreferrer"` on `target="_blank"` anchors (CWE-1022 guard). ## Backlog (deferred) - UX-H2 mermaid latent (no current PRD/RFC uses mermaid; ADR for diagram renderer when needed). - TS-M3..M8, L9..L12 (cosmetic). - SEC-L2 redaction of secret patterns in body excerpt (needs thoughtful regex set — separate artifact). - SEC-L3 kindFilter validation (low impact). - FE-L2..L4 (rare edge cases). ## Verify - `npx svelte-check` — 0/0/436. - `npm test` — 70/70 (68 baseline + 2 new tests for noopener hook + iframe-throw). - `node scripts/smoke.mjs` — PASS. - Playwright DOM check — 0 console errors. ## Test plan - [ ] CI smoke matrix (3-OS × Node 22) green. - [ ] svelte-check clean on PR. - [ ] vitest 70/70. - [ ] Manual: open ArtifactPanel for an Epic with many children — link list scrolls within `max-height: 30vh`. - [ ] Manual: clear all filters in Matrix view with impact-mode active — non-impacted headers visibly fade. - [ ] Manual: open in iframe — Notify toggle does not crash the page. Refs: `PRD-006` `RFC-005` `PRD-007` `RFC-006` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
F11 (body preview + decision impact drill-down — PRD-006/RFC-005), F12 (stale + blind-spot push notifications — PRD-007/RFC-006), F13 (Copy as markdown — tactical), and F14 (post-audit cleanup — 7 HIGH + 7 MEDIUM/LOW from 5-expert audit). Plus the HomePage health-poll effect-cycle hotfix. All artifacts (PRD-006/007 + RFC-005/006) active with R_eff > 0 via EVID-014 / EVID-015. Refs: PRD-006 RFC-005 PRD-007 RFC-006
explosivebit
added a commit
that referenced
this pull request
May 6, 2026
## Summary Back-merge of `release/v0.1.8` (now merged into `main`, tagged `v0.1.8`, published to npm with provenance) into `develop` so the version bump and any release-only commits don't get lost. Per [`guides/GIT-FLOW-GUIDE.ru.md`](guides/GIT-FLOW-GUIDE.ru.md) §6.10. ## What's in this back-merge - `a3d94b6` — `chore(release): bump version to 0.1.8` - `6a8a073` — release PR #42 merge commit (already on main, tagged v0.1.8, npm published). No new code changes — release was already QA'd through PR #42 (3-OS smoke matrix green, npm publish workflow 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.8 rolling up the overnight + audit cycle:
effect_update_depth_exceededin HomePage health-poll diff (prevHealthSnapshotdemoted from$stateto plainlet).marked + DOMPurify(-21 KB gzip first-paint), Matrix impact-mode CSS fix, iframe Notification guards, branded SafeHtml type, clipboard fallback, reduced-motion broadening, DOMPurifynoopenerhook (CWE-1022).Forgeplan artifacts
Verify
Test plan
v0.1.8annotated onmain, Draft GitHub Release → publish firesrelease.yml→npm publish --provenance.release/v0.1.8→developso version bump and any release-only fixes propagate.Backlog (not in this release)
Deferred from F14 audit:
getOrInithelper sweep,Omitpatterns).Strategic deferrals from earlier sessions: time-travel (forgeplan journal slider), semantic search (Lance index), "what changed" diff view, annotation layer.
🤖 Generated with Claude Code