Codify the spec-vs-playbook boundary: cite behavior, don't restate it

This commit is contained in:
2026-06-09 15:02:12 +03:00
parent 81f563a3e7
commit 29b0d1adb0
2 changed files with 74 additions and 46 deletions
+22 -2
View File
@@ -7,7 +7,26 @@
> >
> They are the architectural counterpart to [`docs/spec/`](../spec/): the **spec** says > They are the architectural counterpart to [`docs/spec/`](../spec/): the **spec** says
> _what the app does_ (behavior, acceptance points); this **playbook** says _how we build > _what the app does_ (behavior, acceptance points); this **playbook** says _how we build
> it_ (state, persistence, modals, routing, rendering, inference, relationships). > it_ (state, persistence, modals, routing, rendering, inference, relationships, and the
> visual + interaction language).
## Spec vs playbook: cite, don't restate
The two documents overlap most in §09 (visual) and §10 (interaction), which necessarily talk
about user-facing widgets. At that overlap, one rule keeps them from drifting:
- The **spec owns product behavior** — what features exist, what surfaces appear when, what
the data does. The **playbook owns the _how_** — state shape, persistence, ARIA roles,
keyboard models, focus, tokens, motion.
- **For product behavior, the spec is the source: cite it (`spec §NN`), don't restate it,
and never contradict it.** A playbook bullet may _name_ the behavior in one clause and cite
the spec, then spend its words on the _how_ (the role, the keys, the focus move) and the
_why_ (the council/canon citation). When a playbook bullet and the spec disagree, the
**spec wins** and the bullet is the bug.
_(This bit us once: arch §10 described "keep the library list, title-only" while spec §02
said "replace the entire list with the onboarding canvas" — the two halves were authored from
opposite mental models. The restatement, not the separation, was the leak.)_
## How to use this playbook ## How to use this playbook
@@ -20,7 +39,7 @@
## The documents ## The documents
| # | Doc | Covers | | # | Doc | Covers |
| --- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | --- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 01 | [State & Stores](01-state-and-stores.md) | Zustand stores; one source of truth; selector derivations; central `useAppStore` vs per-feature stores; testable action functions; debounced auto-save. | | 01 | [State & Stores](01-state-and-stores.md) | Zustand stores; one source of truth; selector derivations; central `useAppStore` vs per-feature stores; testable action functions; debounced auto-save. |
| 02 | [Persistence](02-persistence.md) | The infrastructure-adapter boundary; promise-wrapped IndexedDB wrapper; lazy data loading; per-record schema versioning + migration; localStorage prefs with fallback; storage tiers + quota monitoring. | | 02 | [Persistence](02-persistence.md) | The infrastructure-adapter boundary; promise-wrapped IndexedDB wrapper; lazy data loading; per-record schema versioning + migration; localStorage prefs with fallback; storage tiers + quota monitoring. |
| 03 | [Modal System](03-modal-system.md) | Registry + coordinator + shell; one modal at a time; unsaved-change detection via snapshot; focus trap; backdrop/Escape/close dismissal. | | 03 | [Modal System](03-modal-system.md) | Registry + coordinator + shell; one modal at a time; unsaved-change detection via snapshot; focus trap; backdrop/Escape/close dismissal. |
@@ -30,6 +49,7 @@
| 07 | [Naming & Relationships](07-naming-and-relationships.md) | Unique-name enforcement + import auto-suffix; the bidirectional snippet↔dataset name link; rename propagation into specs. | | 07 | [Naming & Relationships](07-naming-and-relationships.md) | Unique-name enforcement + import auto-suffix; the bidirectional snippet↔dataset name link; rename propagation into specs. |
| 08 | [vega/editor Techniques](08-vega-editor-techniques.md) | Reference brief: borrowable Monaco-schema wiring, vega-embed lifecycle, two-tier validation, and data-flow/debounce techniques distilled from the official Vega-Lite editor — plus where we do better. | | 08 | [vega/editor Techniques](08-vega-editor-techniques.md) | Reference brief: borrowable Monaco-schema wiring, vega-embed lifecycle, two-tier validation, and data-flow/debounce techniques distilled from the official Vega-Lite editor — plus where we do better. |
| 09 | [Visual Design Language](09-visual-design.md) | The _visual_ contract: principles inspired by IBM/Carbon, deliberate divergences (square chrome, free color/theming), the token system (Plex type, 8px spacing, role-based color, motion), component conventions, and where to mine the Carbon/IBM source repos for more. Companion: [`visual-specimen.html`](visual-specimen.html). | | 09 | [Visual Design Language](09-visual-design.md) | The _visual_ contract: principles inspired by IBM/Carbon, deliberate divergences (square chrome, free color/theming), the token system (Plex type, 8px spacing, role-based color, motion), component conventions, and where to mine the Carbon/IBM source repos for more. Companion: [`visual-specimen.html`](visual-specimen.html). |
| 10 | [Interaction & Feedback](10-interaction-and-feedback.md) | The _interaction_ contract: the feedback-channel decision table, latency/feedback budgets, the non-happy-path triad, the recovery & data-safety contract, the keyboard/focus contract, and the resolved widget patterns (window splitter, toolbar, segmented controls, selectable lists, search, sort, empty states, modals). Cites `spec/` for behavior; owns the _how_. |
## The non-negotiable layering (every doc assumes this) ## The non-negotiable layering (every doc assumes this)
@@ -18,6 +18,14 @@ IBM Carbon, the **GOV.UK Design System**, the **WAI-ARIA Authoring Practices Gui
interaction decision this doc doesn't cover, consult the council, then record the interaction decision this doc doesn't cover, consult the council, then record the
resolution back here. resolution back here.
**Cite the spec for behavior; own the _how_.** Each `Resolved —` bullet below is the contract
for an interaction's _mechanics_ — the ARIA role, the keyboard model, the focus move — and
records _why_ (the council citation). It is **not** the source for **product behavior**: what
a surface shows, when it appears, what the data does. That lives in `docs/spec/`; a bullet
**names** the behavior in a clause, cites `(spec §NN)`, and never restates or overrides it. On
any disagreement, the spec wins and the bullet is the bug (see `00-overview` → "cite, don't
restate").
--- ---
## 1. The feedback-channel decision table ## 1. The feedback-channel decision table
@@ -224,9 +232,11 @@ forbids interactive children in a listbox. Mark the active row with `aria-curren
**only on that row** (don't emit `aria-current="false"` everywhere). Arrow-key roving **only on that row** (don't emit `aria-current="false"` everywhere). Arrow-key roving
_between_ rows is a later enhancement; button-per-row tab stops are the acceptable baseline. _between_ rows is a later enhancement; button-per-row tab stops are the acceptable baseline.
**Resolved — snippet-list status indicator.** The library row flags only the _exceptional_ **Resolved — snippet-list status indicator.** Spec §02 owns the behavior — the row
state: a single **accent dot** when a snippet has unpublished draft changes; a fully-published distinguishes a snippet with **unpublished draft changes** from a **fully-published** one; this
snippet shows **no dot** (presence = draft, absence = published). We do **not** give "published" bullet owns the _how_. The row flags only the _exceptional_ state: a single **accent dot** for
the unpublished case; a fully-published snippet shows **no dot** (presence = draft, absence =
published). We do **not** give "published"
its own glyph — GOV.UK's Tag guidance notes one status suffices when absence is self-evident, its own glyph — GOV.UK's Tag guidance notes one status suffices when absence is self-evident,
and Carbon's status-indicator pattern says not to highlight what isn't significant. Meaning and Carbon's status-indicator pattern says not to highlight what isn't significant. Meaning
never rests on **hue** (WCAG 1.4.1): it rides on presence/absence **plus** the dot's accessible never rests on **hue** (WCAG 1.4.1): it rides on presence/absence **plus** the dot's accessible
@@ -266,37 +276,35 @@ _(Consulted via /council → WAI-ARIA APG disclosure + menu-button, NN/g #6. Thi
contract; cite it, not the source.)_ contract; cite it, not the source.)_
**Resolved — the empty-library onboarding canvas (and the list's single empty state).** The **Resolved — the empty-library onboarding canvas (and the list's single empty state).** The
snippet list owes **one empty state**: **no search matches** "No snippets match your search" snippet list owes **one empty state** **no search matches** ("No snippets match your search",
with a hint to try a different term). There is **no separate "empty library" list state**,
- a hint to try a different term. There is **no separate "empty library" list state**, because because a genuinely empty library never shows the list at all: rather than seeding a placeholder
a genuinely empty library never shows the list at all: rather than seeding a placeholder
snippet (the old behavior), the workspace replaces the **entire pane chrome — toggle strip, snippet (the old behavior), the workspace replaces the **entire pane chrome — toggle strip,
library list, editor, and preview — with a full-width onboarding canvas** (spec §02 → library list, editor, and preview — with a full-width onboarding canvas** (spec §02 →
First-Run & Empty Workspace): with no snippets, the library controls and pane toggles have First-Run & Empty Workspace): with no snippets, the library controls and pane toggles have
nothing to act on. (So the list's own empty copy is reached only mid-search, never on a cold nothing to act on. (So the list's own empty copy is reached only mid-search, never on a cold
start.) The canvas is a welcome, a primary "Create your first start.) The canvas is a welcome, a primary "Create your first snippet", and a gallery of
snippet", and a gallery of example snippets; leaving it (creating the first snippet) lays the example snippets; leaving it (creating the first snippet) lays the panes out at a default
panes out at a default **25·25·50** split via `PanesStore.applyOnboardingSplit` so the first **25·25·50** split via `PanesStore.applyOnboardingSplit` so the first chart opens with a
chart opens with a generous preview. Each card generous preview. Each card **renders live through the shared `chart-renderer`** (no parallel
**renders live through the shared `chart-renderer`** (no parallel embed path; each card owns embed path; each card owns its `RenderHandle` and finalizes on unmount — the per-card nodes are
its `RenderHandle` and finalizes on unmount — the per-card nodes are independent, so they independent, so they don't touch `LivePreview`'s single-host serialization), and adds as an
don't touch `LivePreview`'s single-host serialization), and adds as an ordinary snippet. ordinary snippet. Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist;
Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist; §3 empty ≠ §3 empty ≠ error). Council resolutions for the canvas: (1) **each card preview is decorative**
error). Council resolutions for the canvas: (1) **each card preview is decorative** `aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative images);
`aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative the card **name + one-line description + a uniquely-labelled `Add` button** ("Add Bar chart",
images); the card **name + one-line description + a uniquely-labelled `Add` button** ("Add APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is the **single
Bar chart", APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is empty surface** — because it replaces the library list outright, there is no competing "No
the **single empty surface** — because it replaces the library list outright, there is no snippets yet" status elsewhere to keep in sync. Its heading **owns the app identity** ("Welcome
competing "No snippets yet" status elsewhere to keep in sync. Its heading **owns the app to Astrolabe") and it carries the only Create nudge, so the empty-state message lives in exactly
identity** ("Welcome to Astrolabe") and it carries the only Create nudge, so the empty-state one place (Carbon "keep words to a minimum"; no duplication). (3) The **primary action
message lives in exactly one place (Carbon "keep words to a minimum"; no duplication). dominates** (accent "Create your first snippet" first; the example gallery is framed as a
(3) The **primary action dominates** (accent "Create your first snippet" first; the example secondary "Or start from an example") — Carbon sanctions starter content as an in-depth
gallery is framed as a secondary "Or start from an example") — Carbon sanctions starter first-use empty state only when one action stays primary. (4) **Domain vocabulary stands**:
content as an in-depth first-use empty state only when one action stays primary. (4) "Vega-Lite", "JSON", "snippet" are kept despite Carbon's avoid-jargon rule, because SOUL #2
**Domain vocabulary stands**: "Vega-Lite", "JSON", "snippet" are kept despite Carbon's (Vega-Lite Native) makes them the user's real language — a deliberate divergence. _(Consulted
avoid-jargon rule, because SOUL #2 (Vega-Lite Native) makes them the user's real language — via /council → Carbon empty-state + content, GOV.UK headings, WAI-ARIA APG button, NN/g. This
a deliberate divergence. _(Consulted via /council → Carbon empty-state + content, GOV.UK bullet is the contract; cite it, not the source.)_
headings, WAI-ARIA APG button, NN/g. This bullet is the contract; cite it, not the source.)_
**Resolved — one live region per shared message.** When the same error feeds two surfaces **Resolved — one live region per shared message.** When the same error feeds two surfaces
(the §1 "one producer, two subscribers" case — render errors via `PreviewStore`), exactly (the §1 "one producer, two subscribers" case — render errors via `PreviewStore`), exactly