Docs: move point-in-time records to exploration/, trim build-narration, lift open items to plan

This commit is contained in:
2026-06-14 23:58:06 +03:00
parent de9dbf3ec8
commit 66c123e15e
16 changed files with 77 additions and 68 deletions
+2 -5
View File
@@ -22,11 +22,8 @@ about user-facing widgets. At that overlap, one rule keeps them from drifting:
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.)_
**spec wins** and the bullet is the bug. Restatement is the leak: two docs describing the
same behavior in their own words drift into contradiction; one cites the other instead.
## How to use this playbook
+7 -32
View File
@@ -30,12 +30,9 @@ A store is a module that calls `create<State>()` once and exports the resulting
hook. The state object holds both **data fields** and **action functions**.
```ts
// src/app/stores/AppStore.ts
import { create } from 'zustand';
import type { UiTheme } from '@core/theme'; // defined in core; charts key off it too
export type ModalName = 'datasets' | 'about' | 'donate' | 'chartBuilder' | 'extract';
// ModalName is the union of the app's modal identifiers; UiTheme is defined in core.
export interface AppState {
uiTheme: UiTheme;
activeModal: ModalName | null;
@@ -297,36 +294,17 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
}));
```
The component is thin — it selects state and calls actions:
The component is thin — it selects state narrowly and wires events to actions, with no
mutation logic of its own:
```tsx
import { useShallow } from 'zustand/react/shallow';
import { useSnippetStore } from '../stores/SnippetStore';
export function SnippetList() {
const { snippets, activeSnippetId } = useSnippetStore(
useShallow((s) => ({ snippets: s.snippets, activeSnippetId: s.activeSnippetId })),
);
const select = useSnippetStore((s) => s.select);
const select = useSnippetStore((s) => s.select); // stable identity — select actions individually
const remove = useSnippetStore((s) => s.remove);
return (
<ul>
{snippets.map((s) => (
<li key={s.id} aria-current={s.id === activeSnippetId} onClick={() => select(s.id)}>
{s.name}
<button
onClick={(e) => {
e.stopPropagation();
remove(s.id);
}}
>
</button>
</li>
))}
</ul>
);
// render: one row per snippet, each calling select(id) / remove(id) on the events.
}
```
@@ -508,11 +486,8 @@ couples every intermediate component to data it doesn't use.
Each store exposes a `reset()` action that returns its fields to initial values
(used on "new workspace", sign-out, or test teardown). Because every fact is a
single source field with no hand-maintained duplicates, reset is a flat `set(...)`
of the initial values; selector-derived values recompute on their own.
```ts
reset: () => set({ snippets: [], activeSnippetId: null, draftSpec: '' });
```
of the initial values (as in `SnippetStore` above); selector-derived values
recompute on their own.
---
+1 -1
View File
@@ -422,7 +422,7 @@ export async function readOriginUsage(): Promise<number | undefined> {
}
```
**The bar is decorative; the legend is the data.** There is no `role="meter"` — a meter needs a meaningful maximum, which a composition with no fixed ceiling lacks (APG → meter). The legend's text labels + sizes are the accessible source of truth, so meaning never rests on hue (WCAG 1.4.1). Dropping the old 0.8/0.95 "almost full" thresholds is deliberate: they keyed off the untrustworthy quota; the genuine out-of-room event surfaces at save time (below), where it is accurate. (Resolves the former spec §02 ↔ code "whole-origin vs. snippet budget" divergence; council resolution recorded in `docs/architecture/10`.)
**The bar is decorative; the legend is the data.** There is no `role="meter"` — a meter needs a meaningful maximum, which a composition with no fixed ceiling lacks (APG → meter). The legend's text labels + sizes are the accessible source of truth, so meaning never rests on hue (WCAG 1.4.1). There are no "almost full" percentage thresholds — a fraction would key off the untrustworthy quota; the genuine out-of-room event surfaces at save time (below), where it is accurate.
### Fail loudly, never silently lose data
@@ -45,7 +45,7 @@ above it is data; everything below it is a Vega `View` we own and must tear down
> the main Live Preview) reads runtime rows via `view.data(name)`. It rides this same
> boundary: a `RenderHandle.data()` accessor wraps the view (like `toImageURL`), so no
> component touches the raw view. Parked, not built — see
> [`docs/data-inspector-exploration.md`](../data-inspector-exploration.md).
> [`docs/exploration/data-inspector-exploration.md`](../exploration/data-inspector-exploration.md).
---
@@ -126,8 +126,7 @@ async function rerender(node: HTMLElement, spec: TopLevelSpec, config: Config) {
crisp/inspectable/copyable and stays the default for the editor's LivePreview. But an
SVG chart renders one DOM node per mark, so a many-mark chart (e.g. the Chart Builder's
default one-bar-per-row on a 10k-row dataset) costs **seconds** of main-thread
layout/paint per render (measured ~6.5s on 9994 rows; the chart paints _after_ it first
appears, freezing the tab). The **Chart Builder preview** therefore passes
layout/paint per render (the chart paints _after_ it first appears, freezing the tab). The **Chart Builder preview** therefore passes
`renderSpec(…, { renderer: 'canvas' })` — canvas is a single node and paints in
milliseconds. The raster trade-off is invisible for an ephemeral preview, and image
export (`view.toImageURL`) is renderer-agnostic.
@@ -165,7 +164,7 @@ background, the categorical color range. Astrolabe ships one config per UI theme
so charts visually belong to the app rather than looking like stock Vega-Lite.
`src/core/vega-themes.ts` is the single source of truth; each house config is
**two merged layers** (the full audit and forward plan live in
[`docs/chart-theming-scope.md`](../chart-theming-scope.md)):
[`docs/exploration/chart-theming-scope.md`](../exploration/chart-theming-scope.md)):
- **Base** (`lightBaseConfig`/`darkBaseConfig`) — the legibility minimum:
`background: 'transparent'` (the pane shows through) plus guide colors on the
@@ -254,12 +254,9 @@ the Draft/Published view) is a **radio group**, never a row of `aria-pressed` to
- `role="radio"`/`aria-checked`, a **roving tabindex** (only the selected option is a tab
stop), and Arrow/Home/End to move-and-select (APG → Radio Group). One widget so the keyboard
model is defined once. _(Tabs were a candidate for Draft/Published; we chose radio group for
consistency with the other segmented controls and to avoid tabpanel wiring to Monaco. A
**toggle switch** was also weighed and rejected: APG defines `role="switch"` as on/off of a
**single** setting, but Draft/Published selects between two **named peer views** with no
natural "on" side — a radio group is the right semantics. Reserve the switch for genuine
on/off settings. Consulted via /council → APG switch / radio-group / tabs.)_ A per-option
model is defined once. _(Not a toggle switch: APG defines `role="switch"` as on/off of a **single** setting, but
Draft/Published selects between two **named peer views** with no natural "on" side — a radio
group is the right semantics. Reserve the switch for genuine on/off settings.)_ A per-option
`title` (tooltip for a terse label) doubles as the option's accessible name, so it must
**lead with the visible label** ("Original — the natural size from the spec"), or
speech-input users can't address the control they see (WCAG 2.5.3 label-in-name).
@@ -302,9 +299,9 @@ show real measured sizes instead.
- **Unavailable degrades, not disappears.** Snippets + datasets are measured from our own data, so
they always show; only the **App** segment (which needs the origin estimate) drops out when the
Storage Manager API is absent.
- **No proactive "almost full" warning.** Dropping the old 0.8/0.95 thresholds is intentional —
they keyed off the untrustworthy quota, and a fake fuel gauge fails NN/g #1 (_visibility of system
status_) more than it serves it. The genuine out-of-room event surfaces at **save time** as an
- **No proactive "almost full" warning.** A percentage gauge would key off the untrustworthy
quota, and a fake fuel gauge fails NN/g #1 (_visibility of system status_) more than it
serves it. The genuine out-of-room event surfaces at **save time** as an
actionable error (`services/storage-errors.ts` → recover by deleting), satisfying NN/g #9.
_(Consulted via /council → WAI-ARIA APG `meter`, FT Visual Vocabulary + Datawrapper (part-to-whole),
@@ -356,7 +353,7 @@ generous preview. Each card **renders live through the shared `chart-renderer`**
embed path; each card owns its `RenderHandle` and finalizes on unmount — the per-card nodes are
independent, so they don't touch `LivePreview`'s single-host serialization), and adds as an
ordinary snippet. Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist;
§3 empty ≠ error). Council resolutions for the canvas: (1) **each card preview is decorative**
§3 empty ≠ error). Three rules govern the canvas. (1) **Each card preview is decorative**
`aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative images);
the card **name + one-line description + a uniquely-labelled `Add` button** ("Add Bar chart",
APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is the **single
@@ -386,7 +383,7 @@ primary next step ("Add a dataset" → the Datasets create form) — never a dea
dataset-row "Build Chart" stays as the contextual shortcut; the onboarding canvas gains the
data-first door ("Build a chart from your data") beside its primary. _(Consulted via
/council → NN/g #6/#7, Carbon empty-states; recorded in
docs/chart-builder-enhancement-scope.md §3 · 3D. This bullet is the contract; cite it, not
docs/exploration/chart-builder-enhancement-scope.md §3 · 3D. This bullet is the contract; cite it, not
the source.)_
**Resolved — one live region per shared message.** When the same error feeds two surfaces