From 5556d500f6a8763e337a2b4993571709adfe0523 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 04:00:02 +0000 Subject: [PATCH 01/10] chore(efforts): refute crumb rebrand, keep Effort Graph as the product name Journals the owner call through the typed effort-graph mutations rather than hand-edited frontmatter: - Finding (dead-end) recording why the crumb naming line does not hold: crumb reads as leftovers, collides with breadcrumb navigation, and hides the Effort primitive that the API, CLI, and skills already teach. Both prior Decisions listed exactly these as reversal criteria. - Decision "Keep Effort Graph as the product name", superseding Crumb Trail (and transitively Crumb Graph), accepted. - Both rename Issues resolved as wontfix, resolved_by the new Decision - the work is cancelled, not postponed. Change-Id: Id0da8e7252e90b4fee9fac2ea10b307fbe5943c8 --- ...urface-as-crumb-trail--tngncepdbwjkh9jc.md | 2 + ...h-as-the-product-name--r5wr2vdjwjs9bs13.md | 43 +++++++++++++++++++ ...eview-effort-graph-is--n8kb269z7vz8jhyk.md | 30 +++++++++++++ ...e-across-packages-cli--dp6jvt2kafab7m4t.md | 4 +- ...e-across-packages-cli--316rdzpt80sdxkw1.md | 4 +- 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .flatbread-efforts/decisions/dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13.md create mode 100644 .flatbread-efforts/findings/fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk.md diff --git a/.flatbread-efforts/decisions/dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc.md b/.flatbread-efforts/decisions/dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc.md index 551729c5..217c5e94 100644 --- a/.flatbread-efforts/decisions/dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc.md +++ b/.flatbread-efforts/decisions/dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc.md @@ -9,6 +9,8 @@ derives_from: - fnd-workshop-shortlist-favors-crumb-graph-over-yeast--tv3ydt3wfb3n4y0g supersedes: - dec-brand-the-agent-memory-surface-as-crumb-graph--fvskcvagx3a7sybe +superseded_by: + - dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13 --- Supersedes Crumb Graph as the product name. Same system and deferred implementation; product brand is now Crumb Trail, with Crumb Graph retained as the datamodel explainer. diff --git a/.flatbread-efforts/decisions/dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13.md b/.flatbread-efforts/decisions/dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13.md new file mode 100644 index 00000000..7f0e8542 --- /dev/null +++ b/.flatbread-efforts/decisions/dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13.md @@ -0,0 +1,43 @@ +--- +id: dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13 +effort: eff-flatbread-product-branding--zt7b35sa05kyvhdz +title: Keep Effort Graph as the product name +state: accepted +created_at: '2026-07-26T03:27:20.788Z' +derives_from: + - fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk +supersedes: + - dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc +--- + +Supersedes Crumb Trail (and, transitively, Crumb Graph). The rebrand line is refuted: **Effort Graph** is the product name for Flatbread's longform agent memory, and crumb naming is dropped entirely rather than retained as a datamodel explainer. + +## Context + +Two prior Decisions moved the brand from Effort Graph to Crumb Graph and then to Crumb Trail, each time deferring the package, CLI, path, and skill renames. Review of the accumulated naming stack found the crumb metaphor works against the product: crumbs connote leftovers in a system whose pitch is durable, trustworthy reasoning, and _crumb trail_ is already taken by breadcrumb navigation in UI vocabulary. Meanwhile every identifier a user touches — `@flatbread/effort-graph`, `flatbread effort`, `.flatbread-efforts/`, the `effort-graph` skill — still said Effort. + +## Decision + +Adopt **Effort Graph** as the product name, not merely an allowed technical descriptor. Retire **Crumb Graph** and **Crumb Trail**; neither is a product name, a datamodel explainer, nor a documented alias. + +Describe the datamodel in plain terms — Effort-scoped relational records with typed edges and bounded digests — instead of coining a second branded layer for it. + +No rename work is required or deferred: code, packages, CLI, paths, and skills already match the product name, and that alignment is now the point rather than a coincidence. + +## Alternatives considered + +- **Keep Crumb Trail as the brand and finally execute the renames.** Rejected: it pays a full breaking-rename cost to buy a metaphor that review judged actively misleading. +- **Keep Crumb Trail for marketing, Effort Graph in code.** Rejected: this is the status quo the prior Decisions created, and it is the failure mode — a permanent gap between what the docs call the product and what every command and import path calls it. +- **Keep Crumb Graph as a datamodel explainer only.** Rejected: a second branded vocabulary for the same graph adds a translation step without adding precision over saying Effort-scoped records and edges. +- **Pick a third unleavened, Proof-peer name.** Rejected for now: naming churn has already cost two Decisions and two stale Issues, and no candidate beats the name that the API already teaches. + +## Consequences + +- Docs, skills copy, and product messaging use Effort Graph with no crumb aliases; existing crumb references are wording bugs to fix, not variants to preserve. +- The rename Issues under this Effort close as wontfix — the work is cancelled, not postponed. +- Brand and implementation vocabulary are unified, so future naming pressure has to argue for renaming real identifiers rather than only marketing copy. +- Flatbread gives up a bread-metaphor peer to Proof for this surface. That is accepted: primitive fidelity beats metaphor symmetry here. + +## Reversal criteria + +Revisit only if Effort Graph measurably fails external comprehension — for example if users read Effort as project-management effort tracking or story points rather than a thread of work — and a candidate name wins while committing to rename the packages, CLI, and paths in the same Decision rather than deferring them again. diff --git a/.flatbread-efforts/findings/fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk.md b/.flatbread-efforts/findings/fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk.md new file mode 100644 index 00000000..85293c71 --- /dev/null +++ b/.flatbread-efforts/findings/fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk.md @@ -0,0 +1,30 @@ +--- +id: fnd-crumb-rebrand-refuted-in-review-effort-graph-is--n8kb269z7vz8jhyk +effort: eff-flatbread-product-branding--zt7b35sa05kyvhdz +title: Crumb rebrand refuted in review; Effort Graph is the retained name +kind: dead-end +created_at: '2026-07-26T03:26:52.792Z' +derives_from: + - fnd-postable-brand-names-favor-trail-over-graph--zzpgaw0kvqtaqmxt + - fnd-workshop-shortlist-favors-crumb-graph-over-yeast--tv3ydt3wfb3n4y0g +--- + +## What happened + +The Crumb Graph → Crumb Trail branding line was grilled in review and refuted. The owner call is to keep **Effort Graph** as the product name for Flatbread's longform agent memory, not as a fallback technical alias. + +## Why the rebrand did not hold + +- **Crumb reads as leftovers, not memory.** In a product about durable reasoning, the metaphor undercuts the value claim: crumbs are what falls off the loaf, not the record you are meant to trust. +- **Crumb collides with breadcrumbs.** In UI vocabulary a crumb trail is a navigation path widget. Users landing on the docs pattern-match to breadcrumb navigation before they reach agent memory. +- **Effort Graph already names the load-bearing primitive.** Every record belongs to exactly one Effort. A brand that hides the Effort primitive forces a second vocabulary on top of the one the API, CLI, and skills already teach. +- **The rebrand's own reversal criteria fired.** Both prior Decisions listed external-comprehension failure and crumb ≈ breadcrumbs/scraps confusion as revisit triggers. Review found exactly those failures, so the Decisions closed themselves out on their stated terms. +- **Deferring implementation hid the cost.** Both Decisions deferred the package/CLI/path/skill renames, which kept the accepted brand permanently out of sync with every identifier a user actually touches. + +## Standing evidence + +The two workshop Findings remain accurate as records of what those exercises produced — the shortlist did favor crumb over yeast metaphors, and trail did test as more postable than graph. Neither is invalidated. What is refuted is treating brandability as decisive over primitive fidelity and collision risk. + +## Consequence + +The rename work tracked by both implementation Issues is a dead end and should close as wontfix rather than stay open as latent debt. diff --git a/.flatbread-efforts/issues/iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t.md b/.flatbread-efforts/issues/iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t.md index 520ea1b1..99c5d9b7 100644 --- a/.flatbread-efforts/issues/iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t.md +++ b/.flatbread-efforts/issues/iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t.md @@ -3,12 +3,14 @@ id: iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t effort: eff-flatbread-product-branding--zt7b35sa05kyvhdz title: 'Implement Crumb Graph rename across packages, CLI, paths, and skills' kind: gap -status: open +status: wontfix created_at: '2026-07-19T03:45:50.712Z' derives_from: - dec-brand-the-agent-memory-surface-as-crumb-graph--fvskcvagx3a7sybe superseded_by: - iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1 +resolved_by: + - dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13 --- Branding Decision accepts Crumb Graph as the product name with Effort Graph as descriptor. Implementation is pending and out of scope for the branding commit. diff --git a/.flatbread-efforts/issues/iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1.md b/.flatbread-efforts/issues/iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1.md index c3179aaf..cbbfb359 100644 --- a/.flatbread-efforts/issues/iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1.md +++ b/.flatbread-efforts/issues/iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1.md @@ -3,12 +3,14 @@ id: iss-implement-crumb-trail-rename-across-packages-cli--316rdzpt80sdxkw1 effort: eff-flatbread-product-branding--zt7b35sa05kyvhdz title: 'Implement Crumb Trail rename across packages, CLI, paths, and skills' kind: gap -status: open +status: wontfix created_at: '2026-07-19T03:54:27.962Z' derives_from: - dec-brand-the-agent-memory-surface-as-crumb-trail--tngncepdbwjkh9jc supersedes: - iss-implement-crumb-graph-rename-across-packages-cli--dp6jvt2kafab7m4t +resolved_by: + - dec-keep-effort-graph-as-the-product-name--r5wr2vdjwjs9bs13 --- Supersedes the Crumb Graph rename Issue. Branding now targets Crumb Trail as the product name (Crumb Graph = datamodel explainer; Effort Graph = technical descriptor). From 223f64c74a73bca25e1e59341d89310d3976e63e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 04:26:59 +0000 Subject: [PATCH 02/10] feat(effort-viz): encode primitive on hue and shape, retired state on canvas The graph could not answer its most basic question: what kind of record is this? `nodeColor` hashed hue from the *effortId* and gave node kind only a +/-0.06 lightness nudge, so every record in an Effort cluster rendered as the same coloured circle. A per-kind palette already existed in `RelationLegend.tsx` and was dead code; the legend drew its six swatches from the broken function, so it showed six identical dots next to six different labels. Encoding - Hue now carries the primitive. Effort membership was already delivered by three channels - the force layout's shared centroid, a large labelled hub, and hub-tinted membership spokes - so it was spending the strongest nominal channel on the attribute that needed it least. - Shape repeats hue as a colour-vision backstop: amber/red and blue/violet partially merge under deuteranopia. Outlines are area-normalized in `lib/glyphs.ts` so a triangle and a square read at equal visual weight; otherwise size would imply a ranking nobody intended. - Effort hubs became neutral rings with a cluster-tinted core. The core also covers the membership spokes converging on the centre, which previously showed through the ring's hole as clutter. - Legend swatches and canvas geometry now derive from the same outlines and palette, so the key cannot drift from the render, and the legend lists only the relations the current generation contains. Lifecycle - `lib/lifecycle.ts` derives effective state from edges. A superseded Decision keeps `state: accepted` in frontmatter, so reading the field labelled retired reasoning as committed - the worst possible error on the record a reader most needs to get right. Retired records now fade with a struck-through label, and the drawer explains why its frontmatter disagrees. - Badges are keyed on `primitive:state`: a Risk's "accepted" means "we chose to live with this hazard" and must not borrow a Decision's reassuring green. Added the missing wontfix, mitigated, realized, and Effort status vocabulary. - Open blocker Issues wear an amber warning outline. Layout and camera - Added cluster separation to the force model. Generic node repulsion separates records but lets clusters interleave, and position is the channel carrying cluster identity. Cluster aggregates moved onto the scratch object, removing three map allocations per frame. - Effort titles anchor above their cluster's bounding box instead of on the hub, which sits at the centroid and put every label on top of the records it names. - Camera eases into place instead of cutting after a 2.4s delay, keeps re-fitting while the layout spreads, and yields permanently once the reader pans or zooms. Only the always-present legend is reserved in the fit; the transient drawer no longer costs a quarter of the canvas. Vocabulary - Aligned with the effort-graph glossary: Primitives not "node kinds", Relations not "Decision chain", relation groups named after the edges the CLI uses, and the relation descriptions rewritten to match the glossary's claims (`invalidates` says a record was *wrong*, not that it is "no longer valid"). - The header counts primitives and lifecycle rather than nodes and edges: half the edges are synthesised membership spokes, and "173 nodes" says a dot appeared where "3 proposed Decisions" says someone owes a call. - Relation rows render `directionHint` instead of a bare arrow glyph. Craft, a11y, and bug fixes - Node retraction never rendered: the scene resolved metadata from the current query result, so a deleted record vanished instantly while its edges withdrew gracefully. Metadata is now cached until the simulation drops the node. - `computeLineDistances()` ran inside `useFrame`, allocating a fresh `Float32BufferAttribute` per dashed edge per frame. Now recomputed only when the vertex count changes. - Selection committed on pointer-down while left-drag pans, so panning from a record opened the drawer. Now commits on pointer-up within an 8px slop. - Records gained an oversized invisible circular hit mesh sized from `viewport.factor`, guaranteeing a 44 CSS px target at any zoom. - Canvas is keyboard reachable: arrow keys walk a stable order, Enter opens, Escape closes, the camera follows focus, and moves are announced politely. - Type floor raised from 9-10px to 11-12px. Labels no longer change font weight on selection (it reflowed text under the pointer) and dropped their per-label backdrop blur. - Status pill has a fixed width and counts use tabular figures, so live updates cannot shift the header. - Theme follows `prefers-color-scheme` at runtime until the user picks a mode, instead of persisting the resolved system value as an explicit choice. - Reduced motion settles the layout before first paint rather than animating every spawn. - Markdown links only become in-graph navigation when the target resolves to a record; fragments and unresolved relative links stayed anchors instead of becoming silent no-op buttons. - Removed dead `ambientLight` (every material is unlit) and moved the atmospheric gradient off `body`, where an opaque app root hid it entirely. Tests: lifecycle derivation and glyph/palette invariants (31 passing). Change-Id: Ia53f416409d71f8f0866d25d6966d800e2ca97bf --- examples/effort-viz/README.md | 69 +- .../app/components/DetailDrawer.tsx | 202 +++-- .../app/components/EffortGraphApp.tsx | 80 +- .../effort-viz/app/components/GraphCanvas.tsx | 840 ++++++++++++++---- examples/effort-viz/app/components/Legend.tsx | 251 ++++-- .../app/components/MarkdownSurface.tsx | 47 +- .../app/components/RelationLegend.tsx | 354 +++++--- examples/effort-viz/app/components/TopBar.tsx | 90 +- examples/effort-viz/app/globals.css | 72 +- examples/effort-viz/app/hooks/useTheme.tsx | 29 +- examples/effort-viz/app/layout.tsx | 3 +- examples/effort-viz/lib/glyphs.test.ts | 88 ++ examples/effort-viz/lib/glyphs.ts | 105 +++ examples/effort-viz/lib/lifecycle.test.ts | 131 +++ examples/effort-viz/lib/lifecycle.ts | 201 +++++ examples/effort-viz/lib/oklch.ts | 75 +- examples/effort-viz/lib/physics/forces.ts | 152 +++- examples/effort-viz/lib/physics/simulation.ts | 10 +- examples/effort-viz/lib/physics/types.ts | 8 + examples/effort-viz/lib/primitives.ts | 132 +++ examples/effort-viz/package.json | 2 +- 21 files changed, 2311 insertions(+), 630 deletions(-) create mode 100644 examples/effort-viz/lib/glyphs.test.ts create mode 100644 examples/effort-viz/lib/glyphs.ts create mode 100644 examples/effort-viz/lib/lifecycle.test.ts create mode 100644 examples/effort-viz/lib/lifecycle.ts create mode 100644 examples/effort-viz/lib/primitives.ts diff --git a/examples/effort-viz/README.md b/examples/effort-viz/README.md index 8d9d39b3..6253aea2 100644 --- a/examples/effort-viz/README.md +++ b/examples/effort-viz/README.md @@ -36,19 +36,66 @@ Flatbread serves GraphQL at **`http://localhost:5057/graphql`**. The app subscribes to **`http://localhost:5057/events`** (SSE) for schema generation updates. -## What you get +## What you can read off it + +Within a few seconds of opening the page you should be able to answer: + +- **What kinds of records are here?** Every primitive has its own hue *and* its + own silhouette — Issues are amber diamonds, Findings blue circles, Decisions + violet squares, Constraints teal hexagons, Risks red triangles, and each + Effort is a ring whose core carries that cluster's tint. +- **What is still live, and what got overturned?** Rejected, superseded, + invalidated, and won't-fix records fade to a desaturated ghost with a + struck-through label. Supersession is derived from the graph's edges, not from + frontmatter — a Decision that has been replaced still records `state: + accepted`, so reading the field alone would label retired reasoning as + committed. +- **What is blocking?** Open Issues with `kind: blocker` wear an amber warning + outline. +- **How much work is tracked?** The header counts primitives and lifecycle + (`5 Efforts · 4 open Issues · 3 proposed Decisions`) rather than nodes and + edges — roughly half the "edges" are synthesised membership spokes, so a raw + edge count flatters the graph without informing anyone. + +## Encoding notes + +Hue belongs to the **primitive**, not the Effort. Effort membership is already +carried by three other channels — the force layout pulls same-Effort records to +a shared centroid, each cluster has a large labelled hub, and membership spokes +take the cluster's tint — so spending the strongest nominal channel on it left +record kind with nothing. Shape repeats hue as a colour-vision backstop, since +amber/red and blue/violet partially merge under deuteranopia. Silhouettes are +area-normalized (`lib/glyphs.ts`) so a triangle and a square read at the same +visual weight; otherwise size would imply an importance ranking nobody +intended. + +The legend derives its swatches from the same outlines and palette the canvas +builds geometry from (`lib/glyphs.ts`, `lib/primitives.ts`), so it cannot drift +from the render, and it only lists the relations the current generation actually +contains. + +## Other features - **Live graph** — `useEffortGraphLive` opens an `EventSource` on `/events`. On `ready` and each `generation` event it refetches the Effort Graph query and updates the canvas. The status pill shows connecting / live / disconnected and the current generation. - **Watch mode** — `flatbread start --watch` reloads content and config changes - under `.flatbread-efforts`. Edit an effort, issue, or finding file and the + under `.flatbread-efforts`. Edit an Effort, Issue, or Finding file and the graph animates in/out without restarting Next. -- **R3F canvas** — orthographic 2D scene with pan/zoom, node labels, edge - “veins”, spawn/retract physics, and a detail drawer on node click. -- **Theme** — sun/moon toggle in the top bar; preference persists in - `localStorage` (`effort-viz-theme`) with a boot script to avoid FOUC. +- **R3F canvas** — orthographic 2D scene with pan/zoom, cluster labels, edge + “veins”, spawn/retract physics, and a detail drawer on record click. +- **Keyboard** — Tab to the canvas, then arrow keys to walk records in a stable + Effort-then-primitive order, Enter to open the drawer, Escape to close. The + camera follows focus and each move is announced to screen readers. The canvas + itself is still a WebGL surface, so this is a focus proxy rather than a full + DOM mirror of the graph. +- **Reduced motion** — `prefers-reduced-motion` settles the layout and finishes + every growth animation before the first paint, and the camera snaps instead + of easing. +- **Theme** — sun/moon toggle in the top bar. The app follows + `prefers-color-scheme` until you pick a mode, after which the choice persists + in `localStorage` (`effort-viz-theme`) with a boot script to avoid FOUC. ## Scripts @@ -58,7 +105,7 @@ updates. | `pnpm play:efforts` | Same as `dev`, from the monorepo root. | | `pnpm --filter effort-viz build` | `flatbread start` wrapping `next build` (Flatbread must be up during the build). | | `pnpm --filter effort-viz start` | Production Next only (`next start`); run Flatbread separately if needed. | -| `pnpm --filter effort-viz test` | Physics/simulation unit tests under `lib/physics/`. | +| `pnpm --filter effort-viz test` | Unit tests: physics/simulation, normalizer, lifecycle derivation, glyph invariants. | | `pnpm --filter effort-viz exec tsc --noEmit` | Typecheck without running dev servers. | ## Configuration @@ -74,7 +121,13 @@ updates. - `app/` — layout, theme tokens, R3F canvas and UI chrome - `app/hooks/useTheme.tsx` — light/dark context + FOUC boot script - `app/components/` — `EffortGraphApp`, `GraphCanvas`, `TopBar`, `Legend`, - `DetailDrawer` + `DetailDrawer`, `RelationLegend` (shared relation + badge metadata) +- `lib/primitives.ts` — per-primitive label, hue, and glyph: the encoding's + single source of truth +- `lib/glyphs.ts` — area-normalized glyph outlines shared by the canvas and the + legend +- `lib/lifecycle.ts` — effective lifecycle derived from edges, plus the header + summary - `lib/physics/` — force simulation, growth, and layout helpers - `lib/query.ts` — Effort Graph GraphQL query - `flatbread.config.js` — Effort Graph content preset diff --git a/examples/effort-viz/app/components/DetailDrawer.tsx b/examples/effort-viz/app/components/DetailDrawer.tsx index de54246f..21fbed32 100644 --- a/examples/effort-viz/app/components/DetailDrawer.tsx +++ b/examples/effort-viz/app/components/DetailDrawer.tsx @@ -1,16 +1,20 @@ 'use client'; -import { useMemo } from 'react'; -import { nodeColor } from '@/lib/oklch'; +import { useEffect, useMemo, useRef } from 'react'; +import { oklchCss, effortColor, retiredOklch } from '@/lib/oklch'; +import { PRIMITIVES, primitiveOklch } from '@/lib/primitives'; +import { effectiveLifecycle, type LifecycleIndex } from '@/lib/lifecycle'; import type { GraphEdge, GraphNode } from '@/lib/types'; import { useTheme } from '../hooks/useTheme'; import { MarkdownSurface } from './MarkdownSurface'; import { + PrimitiveGlyph, RELATION_GROUP_LABEL, RELATION_GROUP_ORDER, RELATION_META, RelationLineSample, lifecycleBadge, + relationStrokeOklch, type RelationGroupId, } from './RelationLegend'; @@ -18,19 +22,11 @@ interface DetailDrawerProps { node: GraphNode | null; edges: GraphEdge[]; nodesById: Map; + lifecycleIndex: LifecycleIndex; onClose: () => void; onSelect: (id: string | null) => void; } -const KIND_LABEL: Record = { - effort: 'Effort', - issue: 'Issue', - finding: 'Finding', - decision: 'Decision', - constraint: 'Constraint', - risk: 'Risk', -}; - type DirectedEdge = GraphEdge & { direction: 'outgoing' | 'incoming' }; interface GroupedRelations { @@ -42,10 +38,22 @@ export function DetailDrawer({ node, edges, nodesById, + lifecycleIndex, onClose, onSelect, }: DetailDrawerProps) { const { mode } = useTheme(); + const closeRef = useRef(null); + + // Escape closes from anywhere, including while the graph canvas has focus. + useEffect(() => { + if (!node) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [node, onClose]); const groupedRelations = useMemo((): GroupedRelations[] => { if (!node) return []; @@ -76,74 +84,86 @@ export function DetailDrawer({ }); }, [node, edges]); - const navigateTarget = useMemo(() => { + const resolveRecord = useMemo(() => { const bySlug = new Map(); - const byId = new Map(); for (const n of nodesById.values()) { - byId.set(n.id, n.id); if (n.slug) bySlug.set(n.slug, n.id); } - return (target: string) => { - const id = byId.get(target) ?? bySlug.get(target); - if (id) onSelect(id); - }; - }, [nodesById, onSelect]); + return (target: string): string | null => + (nodesById.has(target) ? target : bySlug.get(target)) ?? null; + }, [nodesById]); if (!node) return null; - const lifecycle = node.lifecycle ?? node.status ?? node.state; - const badge = lifecycleBadge(lifecycle); + const primitive = PRIMITIVES[node.kind]; + const life = effectiveLifecycle(node, lifecycleIndex); + const badge = lifecycleBadge(node.kind, life.state); + const retired = life.aliveness === 'retired'; const hasRelations = groupedRelations.length > 0; const hasBody = Boolean(node.body?.trim()); - const effortId = node.effortId ?? node.id; - const swatch = nodeColor(effortId, node.kind, mode).css; + + const tint = + node.kind === 'effort' + ? effortColor(node.id, mode).css + : oklchCss( + retired + ? retiredOklch(primitiveOklch(node.kind, mode), mode) + : primitiveOklch(node.kind, mode) + ); return ( -