@nurix/etna 0.20.37 → 0.20.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/data/catalog/rules/frontend.md +5 -5
  2. package/data/catalog/rules/pnpm.md +2 -2
  3. package/data/harness-index.json +1 -1
  4. package/data/harnesses/code/agents/brainstorm.md +5 -3
  5. package/data/harnesses/code/codex/agents/brainstorm.toml +5 -3
  6. package/data/harnesses/code/harness.json +2 -2
  7. package/data/harnesses/code/hooks/approved-packages.md +1 -2
  8. package/data/harnesses/code/rules/frontend.md +5 -5
  9. package/data/harnesses/code/rules/pnpm.md +2 -2
  10. package/data/harnesses/code/skills/boundary/references/packs/build-config-runtime.md +1 -1
  11. package/data/harnesses/code/skills/brainstorm/README.md +18 -45
  12. package/data/harnesses/code/skills/brainstorm/SKILL.md +105 -173
  13. package/data/harnesses/code/skills/brainstorm/references/doctrine.md +70 -141
  14. package/data/harnesses/code/skills/brainstorm/references/entry.md +70 -0
  15. package/data/harnesses/code/skills/brainstorm/references/harvest.md +85 -137
  16. package/data/harnesses/code/skills/brainstorm/references/session-schema.md +98 -135
  17. package/data/harnesses/code/skills/design/components/charts.md +121 -43
  18. package/data/harnesses/code/skills/design/components/collections.md +209 -41
  19. package/data/harnesses/code/skills/design/components/feedback.md +86 -25
  20. package/data/harnesses/code/skills/design/components/hooks.md +198 -12
  21. package/data/harnesses/code/skills/design/components/index.md +93 -71
  22. package/data/harnesses/code/skills/design/components/inputs.md +249 -157
  23. package/data/harnesses/code/skills/design/components/layout.md +98 -38
  24. package/data/harnesses/code/skills/design/components/media.md +46 -25
  25. package/data/harnesses/code/skills/design/components/motion.md +56 -46
  26. package/data/harnesses/code/skills/design/components/navigation.md +54 -15
  27. package/data/harnesses/code/skills/design/components/overlays.md +51 -41
  28. package/data/harnesses/code/skills/design/components/setup.md +84 -5
  29. package/data/harnesses/code/skills/design/components/typography.md +72 -22
  30. package/data/harnesses/code/skills/design/components/utils.md +47 -13
  31. package/data/harnesses/code/skills/design/data/chrome/board-column.md +12 -12
  32. package/data/harnesses/code/skills/design/data/chrome/data-table.md +6 -6
  33. package/data/harnesses/code/skills/design/data/chrome/side-panel.md +15 -15
  34. package/data/harnesses/code/skills/design/data/chrome/sidebar.md +25 -25
  35. package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +22 -22
  36. package/data/harnesses/code/skills/design/data/chrome/topbar.md +12 -12
  37. package/data/harnesses/code/skills/design/references/emission.md +6 -6
  38. package/data/harnesses/code/skills/design/scripts/README-validator.md +20 -8
  39. package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +15 -10
  40. package/data/harnesses/delivery/agents/brainstorm.md +5 -3
  41. package/data/harnesses/delivery/codex/agents/brainstorm.toml +5 -3
  42. package/data/harnesses/delivery/harness.json +2 -2
  43. package/data/harnesses/delivery/skills/brainstorm/README.md +18 -45
  44. package/data/harnesses/delivery/skills/brainstorm/SKILL.md +105 -173
  45. package/data/harnesses/delivery/skills/brainstorm/references/doctrine.md +70 -141
  46. package/data/harnesses/delivery/skills/brainstorm/references/entry.md +70 -0
  47. package/data/harnesses/delivery/skills/brainstorm/references/harvest.md +85 -137
  48. package/data/harnesses/delivery/skills/brainstorm/references/session-schema.md +98 -135
  49. package/data/harnesses/etna/agents/brainstorm.md +5 -3
  50. package/data/harnesses/etna/codex/agents/brainstorm.toml +5 -3
  51. package/data/harnesses/etna/harness.json +2 -2
  52. package/data/harnesses/etna/skills/boundary/references/packs/build-config-runtime.md +1 -1
  53. package/data/harnesses/etna/skills/brainstorm/README.md +18 -45
  54. package/data/harnesses/etna/skills/brainstorm/SKILL.md +105 -173
  55. package/data/harnesses/etna/skills/brainstorm/references/doctrine.md +70 -141
  56. package/data/harnesses/etna/skills/brainstorm/references/entry.md +70 -0
  57. package/data/harnesses/etna/skills/brainstorm/references/harvest.md +85 -137
  58. package/data/harnesses/etna/skills/brainstorm/references/session-schema.md +98 -135
  59. package/data/harnesses/hr/harness.json +1 -1
  60. package/package.json +2 -2
@@ -1,194 +1,157 @@
1
- # Session schema — the single format authority
1
+ # Session schema — the on-disk contract
2
2
 
3
- Load when: bootstrapping a session, writing any working-surface file, dispatching the twin, or
4
- interpreting detector output. The detector (`scripts/detector.mjs`) is the mechanical half of this
5
- contract: every rule below carrying a rule ID is machine-checked; everything else is the twin's
6
- semantic judgment.
3
+ Load when: bootstrap; writing a capture entry or a dispatch line you are unsure of; relaying a detector finding. The twin reads it on every dispatch.
7
4
 
8
- ## Folder layout
5
+ This is the format `scripts/detector.mjs` parses, and the twin, the replay evals and the maintenance census all key on its rule ids. A line that drifts from it fails a rule or, worse, is silently not seen, so match the shapes below exactly: field names, the em dash ` — `, the arrow ` → `, the middle dot ` · `. **Every datetime is UTC in one shape, `YYYY-MM-DDTHH:MMZ`, taken from `date -u +%Y-%m-%dT%H:%MZ`:** the detector compares stamps as strings, so a local-time or differently shaped stamp makes a fresh compose read as stale, or a stale one as fresh.
9
6
 
10
- An open session is `docs/research/{topic}-brainstorm/` (kebab-case topic slug):
7
+ ## Layout
11
8
 
12
- | Path | Holds | Writer |
13
- | --- | --- | --- |
14
- | `state.md` | anchor + aspect map | twin |
15
- | `thesis.md` | composed claim → argument → what changed | twin |
16
- | `forks.md` | open questions with close conditions | twin |
17
- | `ledger.md` | commitments | twin |
18
- | `basis/` | frozen entry material | facilitator, once, at intake |
19
- | `fragments/` | harvest-native fragments, drafted in destination format; created on first real content | twin |
20
- | `ops/` | the untracked capture layer, deleted at stop | — |
21
- | `ops/.gitignore` | `*` — `ops/` **never** enters git [BS-OPS-01/02] | facilitator |
22
- | `ops/capture.md` | the facilitator's raw append ledger | facilitator |
23
- | `ops/sync-log.md` | the paired twin-dispatch log | both (see below) |
24
- | `ops/manifest.md` | the harvest manifest | twin, in the stop pipeline |
25
-
26
- On a passed harvest the folder **moves** to `docs/history/brainstorms/{topic}/`, `ops/` deleted
27
- first. The move is the completion marker; the user then deletes or commits the folder. A folder
28
- under `docs/research/` is by definition unfinished.
29
-
30
- **Write protocol (one writer per file; the sync log is paired):** the facilitator bootstraps `ops/` at intake, appends to
31
- `ops/capture.md`, and writes the initial `basis/` copy plus the sync-log *dispatch* lines —
32
- **nothing else, ever**. The twin writes every other file, at gates. The user may hand-edit anything;
33
- the twin diffs each file against its own last compose before rewriting and folds foreign edits in
34
- as first-class user signal.
35
-
36
- ## state.md
9
+ ```
10
+ docs/research/{topic}-brainstorm/
11
+ ├── state.md twin
12
+ ├── thesis.md twin
13
+ ├── forks.md twin
14
+ ├── ledger.md twin
15
+ ├── fragments/{name}.md twin, created on first real content
16
+ ├── basis/ facilitator, at intake; frozen
17
+ └── ops/ untracked; deleted at the move
18
+ ├── .gitignore facilitator, at bootstrap: the single line *
19
+ ├── capture.md facilitator, every turn
20
+ ├── sync-log.md dispatch lines: facilitator · result lines: twin
21
+ └── manifest.md twin, at stop
22
+ ```
23
+
24
+ On a passing harvest the folder moves to `docs/history/brainstorms/{topic}/` without `ops/`, and `state.md` reads `Status: harvested`.
25
+
26
+ ## `state.md`
37
27
 
38
28
  ```markdown
39
29
  # State
40
30
 
41
- - Status: open | stopping | harvested
42
- - Posture: mid-probe on the sync-layer aspect, three forks open
31
+ - Status: open
32
+ - Posture: mid-probe on the persistence aspect, two forks open
43
33
  - Started: 2026-07-08
44
- - Sitting: single | multi
45
- - Last compose: 2026-07-08T14:30Z | never
34
+ - Sitting: single
35
+ - Last compose: 2026-07-08T14:30Z
46
36
 
47
37
  ## Aspect map
48
38
 
49
- - persistence — how session state survives restarts → F1, D3
39
+ - persistence — how cache state survives restarts → F1, D1
50
40
  - onboarding — the first-run experience → thesis
51
- - risks — failure modes and mitigations → none yet
41
+ - risks — failure modes and mitigations → F2, fragment:domain-model
52
42
  ```
53
43
 
54
- - `Status` moves **only** forward: `open → stopping → harvested` — the twin sets `stopping` when the
55
- stop pipeline begins and `harvested` at the move [BS-STATE-01].
56
- - The aspect map is derived per idea from the basis and edited by the user. Each aspect carries
57
- `→` refs to the content covering it: fork IDs (`F#`), ledger IDs (`D#`), `thesis`,
58
- `fragment:{name}`, or `none yet` (an open coverage gap — informational mid-session
59
- [BS-ASPECT-01], blocking at the manifest gate [BS-ASPECT-02]). Ref *presence* is the detector's
60
- half; whether the content actually covers the aspect is the twin's.
61
-
62
- ## thesis.md
44
+ | Field | Values |
45
+ | --- | --- |
46
+ | `Status` | `open` → `stopping` → `harvested`, forward only: the twin sets `stopping` when the stop pipeline begins and `harvested` at the move |
47
+ | `Posture` | one line a resuming reader can act on: where the conversation is and what is next; a parked session says so here |
48
+ | `Started` | ISO date |
49
+ | `Sitting` | `single` · `multi` |
50
+ | `Last compose` | the ISO datetime of the last thesis compose, equal to the thesis stamp; `never` before the first |
63
51
 
64
- ```markdown
65
- # {Topic} — thesis
52
+ Each aspect line is `- {name} — {gloss} → {refs}`, refs comma-separated: `F<n>` (a fork, open or resolved by a decision), `D<n>`, `fragment:{name}` (a file under `fragments/`), `thesis`, or `none yet`.
66
53
 
67
- Current as of: 2026-07-08T14:30Z
54
+ ## `thesis.md`
68
55
 
69
- ## Claim
70
- ## Argument
71
- ## What changed
72
- ```
56
+ A title, then `Current as of: <ISO datetime>`, then the sections `## Claim`, `## Argument`, `## What changed`, in that order.
73
57
 
74
- Composed prose, rewritten whole at every compose, **never** appended. The three sections and the
75
- stamp are mandatory [BS-THESIS-01]; the stamp must equal `state.md`'s `Last compose`
76
- [BS-COMPOSE-01]. At the manifest gate, `Last compose` must be later than the newest
77
- `ops/capture.md` entry — a compose over unsynced capture is stale [BS-COMPOSE-02].
78
-
79
- ## forks.md
58
+ ## `forks.md`
80
59
 
81
60
  ```markdown
82
- ## F3 — Should reads wait for a compose? [open]
61
+ ## F2 — What happens on catalog API timeout? [open]
83
62
 
84
- Full self-contained prose in the user's framing.
63
+ The cache can serve stale on timeout or propagate the error; the basis is silent on it.
85
64
 
86
- - Raised by: user | twin (S4) | probe | harvest
87
- - Closes when: a pick between wait-for-fresh and read-stale-with-warning
65
+ - Raised by: twin (S1)
66
+ - Closes when: an accepted failure behavior — stale-serve with a warning, or error propagation
88
67
  ```
89
68
 
90
- - IDs `F1, F2, …` unique, never reused [BS-FORK-01]. Status marker: `[open]` or
91
- `[parked → {destination}]`. Every fork carries `Closes when:` [BS-FORK-01].
92
- - `Raised by: twin (S#)` must cite a reconciled sync-log entry [BS-SYNC-02].
93
- - **The twin deletes a resolved fork's entry** — the resolving ledger entry's `Resolves:` field is
94
- the record. `forks.md` holds only open and parked forks; a ledger `Resolves: F3`
95
- coexisting with an open F3 is an inconsistency [BS-FORK-02]. Parked forks survive to the
96
- manifest, which must route each one [BS-MANIFEST-01].
97
- - The manifest gate requires zero `[open]` forks [BS-FORK-03].
69
+ The heading's bracket is `[open]` or `[parked → {destination path}]`. `Raised by:` is `user`, `facilitator`, or `twin (S<n>)` citing the reconciled sync that raised it; a blindspot fork's body opens `Blindspot:`. A fork resolved by a decision leaves the file; the decision's `Resolves:` keeps its id.
98
70
 
99
- ## ledger.md
71
+ ## `ledger.md`
100
72
 
101
73
  ```markdown
102
- ## D4 — Sessions initialize in dump or interview
103
-
104
- - Decision: every session opens in dump or interview; probe and converge are mid-session moves.
105
- - Why: a first-time user cannot be asked to pick a posture they have no vocabulary for.
106
- Declined: free initial posture — observed cold-starts stall in probe; re-evaluate if
107
- session-replay evals show experienced users fighting the default.
108
- - How to apply: the facilitator's start calibration offers only the two entry moves.
109
- - Resolves: F2
74
+ ## D1 — Cache keys are per-tenant
75
+
76
+ - Decision: every cache key embeds the tenant id; no shared entries.
77
+ - Why: cross-tenant reads are a trust break, and memory cost is acceptable at current tenant
78
+ counts. Declined: shared keys with tenant checks at read time — a single missed check leaks
79
+ data; re-evaluate if memory pressure forces it.
80
+ - How to apply: the cache module's key builder prefixes the tenant slug.
81
+ - Resolves: F3
110
82
  ```
111
83
 
112
- - IDs `D1, D2, …` unique, append-only [BS-LEDGER-01]. `Decision`, `Why`, `How to apply` are
113
- mandatory; `Resolves:` when the commitment closes forks. Amendments and reversals are
114
- annotations plus a new entry ("Reversed by D9" on the old; D9 names what it reverses) — never a
115
- rewrite. Declined options live inside `Why` per [doctrine.md](doctrine.md).
116
-
117
- ## ops/capture.md
84
+ `Decision`, `Why` and `How to apply` are required and non-empty; `Resolves` is optional. Ids are unique and never reused.
118
85
 
119
- The facilitator's per-turn append. Lowest possible bar: raw, unpolished, everything
120
- session-relevant from *both* sides (user substance and facilitator substance — diagnoses, informed
121
- rejections, definitions, reasoning chains), including residue judged not worth pillar filing.
122
- Format:
86
+ ## `ops/capture.md`
123
87
 
124
88
  ```markdown
89
+ # Capture
90
+
125
91
  ---
126
- ### T14 · 2026-07-08T14:22Z
92
+ ### T2 · 2026-07-08T14:20Z
127
93
 
128
- <raw notes for this turn>
94
+ facilitator: flagged the trust concern on shared entries — a missed tenant check at read time is
95
+ a leak, not a bug. recommended per-tenant keys; user accepted (→ D1). residue: user mentioned a
96
+ "warm-on-deploy" idea in passing, not pursued.
129
97
  ```
130
98
 
131
- Turn IDs `T#` ascend, each with an ISO timestamp — the timestamp compose-freshness checks against
132
- [BS-COMPOSE-02]. **Never** distill here; distillation is the twin's.
133
-
134
- ## ops/sync-log.md
99
+ Entries are separated by `---`, headed `### T<n> · <ISO datetime>`, numbered in order, and labelled by speaker inside (`user (dump):`, `facilitator:`). The heading's datetime is what the manifest gate compares against the last compose.
135
100
 
136
- Paired-written, one pair per twin dispatch:
101
+ ## `ops/sync-log.md`
137
102
 
138
103
  ```
139
- S4 | gate: on-demand | dispatched: 2026-07-08T14:25Z | scope: full reconcile + compose
140
- S4 → reconciled | 2026-07-08T14:29Z | 4 edits · 2 new forks · 6 open · untouched: risks
104
+ S1 | gate: start | dispatched: 2026-07-08T14:05Z | scope: bootstrap reconcile
105
+ S1 → reconciled | 2026-07-08T14:08Z | 2 edits · 1 new fork · 2 open · untouched: risks
141
106
  ```
142
107
 
143
- The **facilitator** writes the dispatch line as it dispatches; the **twin** appends the result
144
- line (`reconciled` or `failed`) on return — **never** one author for both sides. Every dispatch
145
- line must acquire a result line; only the newest entry may be unpaired (in flight) [BS-SYNC-01].
146
- A `failed` result is surfaced to the user at the next turn break and re-run at the next gate.
108
+ The facilitator writes the first line as it dispatches; the twin appends the second on return, `reconciled` or `failed | <ts> | <reason>`. Gates: `start` · `on-demand` · `pause` · `stop`. Ids run `S1, S2, …` across the session.
147
109
 
148
- ## ops/manifest.md
149
-
150
- Created by the stop pipeline; gates the harvest apply:
110
+ ## `ops/manifest.md`
151
111
 
152
112
  ```markdown
153
113
  # Harvest manifest — {topic}
154
114
 
155
115
  Round: 1
156
- Status: proposed | approved | applied
116
+ Status: proposed
157
117
 
158
118
  | # | What | Source | Destination | Action |
159
119
  |---|------|--------|-------------|--------|
160
- | 1 | consolidated source of truth | thesis + fragments + ledger | docs/features/{x}.md | create |
161
- | 2 | parked fork F7 | forks.md | docs/architecture/open-questions.md | append |
120
+ | 1 | consolidated source of truth | thesis + ledger | docs/features/widget-cache.md | create |
121
+ | 3 | parked fork F2 | forks.md | docs/architecture/open-questions.md | append |
162
122
 
163
123
  ## Uselessness test
164
124
 
165
- - Result: pass | fail
166
- - Cold-reader allowed paths: <the destination docs above>
167
- - Gaps: none | <named gaps → next Round>
125
+ - Result: pass
126
+ - Cold-reader allowed paths: docs/features/widget-cache.md, docs/architecture/open-questions.md
127
+ - Gaps: none
168
128
  ```
169
129
 
170
- Every parked fork and every fragment must have a row [BS-MANIFEST-01]. `Status: approved` is set
171
- only after the user approves; gap fixes re-enter as a new `Round` with the same approval
172
- requirement. After apply, no destination doc may link into the session folder [BS-HARVEST-01];
173
- the move to history requires a recorded passing uselessness test [BS-USELESS-01].
174
-
175
- ## Detector
130
+ `Status` runs `proposed` → `approved` → `applied`. Every parked fork's id and every fragment's file name appear in a row's What or Source. Destinations are repository-relative paths. A gap round increments `Round`.
176
131
 
177
- `node scripts/detector.mjs <session-folder> [--gate=session|manifest|move] [--json]`
132
+ ## The detector
178
133
 
179
- - `session` (default) — the always-on integrity rules; run at any gate.
180
- - `manifest` — adds the convergence gates (zero open forks, full aspect coverage, fresh compose);
181
- the twin may not construct a manifest while any of these fail.
182
- - `move` — adds the post-apply gates (manifest applied, uselessness pass recorded, no links into
183
- scaffolding); the folder may not move while any fail.
134
+ `node <skill>/scripts/detector.mjs <session-folder> [--gate=session|manifest|move] [--json]` exits `0` clean, `1` with violations listed by rule id, `2` on unusable input. Gates are cumulative: `manifest` runs the session rules too, `move` runs all. A real run always prints per-rule lines and a `clean` or `failing` summary; **empty output means the detector never ran**, which is a failed gate, never a pass.
184
135
 
185
- | Exit | Meaning |
186
- | --- | --- |
187
- | `0` | clean |
188
- | `1` | violations, listed with rule IDs |
189
- | `2` | unusable input |
190
-
191
- A run always prints output — per-rule lines plus a `clean`/`failing` summary, or a usage/error
192
- message. **Exit 0 with empty output means the detector never executed** — a failure, **never** a
193
- pass. Rules degrade gracefully: with no git context, `BS-OPS-02` reports `skipped` and never blocks.
194
- Rule IDs are stable — the twin, the evals, and the maintenance census key on them.
136
+ | Rule | Gate | Fails when |
137
+ | --- | --- | --- |
138
+ | `BS-STATE-01` | session | `state.md` missing, or a field absent or invalid |
139
+ | `BS-STATE-02` | session | no `## Aspect map`, or no parseable aspect |
140
+ | `BS-ASPECT-01` | session | an aspect ref names no such fork, decision or fragment |
141
+ | `BS-THESIS-01` | session | the thesis lacks its stamp or a mandated section |
142
+ | `BS-COMPOSE-01` | session | the thesis stamp differs from `Last compose` |
143
+ | `BS-FORK-01` | session | duplicate fork id, no `Closes when:`, or a park without destination |
144
+ | `BS-FORK-02` | session | a decision resolves a fork still open |
145
+ | `BS-LEDGER-01` | session | duplicate decision id, or Decision, Why or How to apply empty |
146
+ | `BS-OPS-01` | session | `ops/.gitignore` missing or not `*` |
147
+ | `BS-OPS-02` | session | any `ops/` file tracked by git (skipped outside git) |
148
+ | `BS-SYNC-01` | session | a dispatch other than the newest has no result line |
149
+ | `BS-SYNC-02` | session | a `twin (S<n>)` attribution cites a sync that never reconciled |
150
+ | `BS-ASPECT-02` | manifest | an aspect has no content (`none yet`) |
151
+ | `BS-COMPOSE-02` | manifest | the newest capture entry is newer than the last compose |
152
+ | `BS-FORK-03` | manifest | any fork is open |
153
+ | `BS-MANIFEST-01` | manifest | a parked fork or a fragment has no manifest row |
154
+ | `BS-HARVEST-01` | move | a destination is missing, or links into the session folder |
155
+ | `BS-USELESS-01` | move | no passing uselessness test with `Gaps: none`, or the manifest is not `applied` |
156
+
157
+ When you relay a finding, name the rule id and what it means for the user, in plain words: "the last compose predates the newest capture (`BS-COMPOSE-02`), so the twin syncs before the manifest".
@@ -2,75 +2,153 @@
2
2
 
3
3
  > Generated reference (regen-owned; see [index.md](index.md)). Binds only under `primitives: nurix`.
4
4
 
5
- Three tiers, all consuming the canonical `ChartSeries[]` / `ChartPoint` data contract (re-exported from `@nurix/components/charts`) and resolving color through tokens — no chart takes hex literals as its primary color path.
5
+ Components: Spark, Chart, ChartErrorCard, StatusTimeline, plus the chart kernel helpers
6
6
 
7
- - **Tier 0 — marks only:** `spark`
8
- - **Tier 1 — inline charts:** `bar-graph`, `line-graph`, `pie-graph`, `progress-bar`, `sparkline`, plus the standalone families `funnel`, `gauge`, `heatmap`, `radar`, `ring`; shared chrome in `chart-primitives`
9
- - **Tier 2 — composable dashboard shells:** the `charts` subpath (visx-based)
7
+ Four entry points, all resolving colour through the `--chart-*` tokens — no chart takes a hex literal as its colour path:
10
8
 
11
- > The old `chart` subpath (a dead recharts wrapper) was removed with the 2026-07 chart adoption — it no longer exists; the `charts` family below replaced it.
9
+ - **Tier 0 — marks only:** `Spark` (`element/spark`) — an axis-less inline trend.
10
+ - **Tier 1 — spec-native charts:** `Chart` (`block/chart`) — one component rendering fourteen families from a serializable `{ config, theme?, data? }` spec.
11
+ - **Status strip:** `StatusTimeline` (`compound/status-timeline`) — one stacked bar per day, no axis; not a bar chart.
12
+ - **Kernel:** `lib/chart-kernel` (+ `/arc`, `/colors`, `/line`) — the data contract, colour policy, formatters and geometry, for a bespoke mark neither `Chart` nor `Spark` draws.
12
13
 
13
14
  ## Spark
14
15
 
15
- Tier-0 inline mark — no axes, no chrome. Import from `@nurix/components/element/spark`.
16
+ Tier-0 trend mark for a stat card or a caption — no axes, no legend, no tooltip, never a tab stop. Import from `@nurix/components/element/spark`. Install the `spark` peer family first: `@mantine/charts @mantine/core recharts`, plus `@mantine/hooks` (which `@mantine/core` peers on); no `MantineProvider` or Mantine stylesheet is needed.
16
17
 
17
- **Props:** `series` (`ChartSeries[]` or plain `number[]`), `type` [line|bar|pie], `filled`, `fillOpacity`, `strokeWidth` (line), `gap` (bar), `width`, `height`, `useTokens` (default false: self-contained oklch; true: `--chart-N` tokens)
18
+ **Props:** `values` (`(number | null)[]` — a `null` is a gap, never a zero), `family` [line|area|bar|pie] (default `line`), `width` / `height` (default `100`×`30` px; a class such as `w-full` overrides the width), `color` (any CSS colour, default `currentColor`), `strokeWidth` (line and area, default `1.5`), `gap` (bar, default `2`), `colors` (pie, per slice; default the `--chart-N` palette), `animate` (default `false`), `label`
19
+
20
+ - The props are a union on `family`: `strokeWidth` on bar, `gap` on line or area, `colors` off pie, and `width` or `color` on pie are type errors. A pie is a `height`×`height` square.
21
+ - It paints in `currentColor`, so a muted caption gives a muted spark; pass `color="var(--chart-1)"` for a token colour.
22
+ - It is `aria-hidden` until `label` names it (then `role="img"`) — a trend beside its number is decorative; a spark standing alone wants a label.
23
+ - A table's sparkline column is `block/table`'s `TableSparkline`, never a hand-embedded `Spark`.
18
24
 
19
25
  ```tsx
20
- <Spark series={[4, 6, 5, 9, 7, 12]} type="line" filled width={96} height={24} />
26
+ <Spark values={[4, 6, null, 9, 7, 12]} family="area" color="var(--chart-1)" className="w-full" />
21
27
  ```
22
28
 
23
- ## Inline charts
29
+ ## Chart
30
+
31
+ The spec-native chart. Import `Chart` from `@nurix/components/block/chart`. Install the `charts` and `animated-number` peer families first: `@visx/curve @visx/event @visx/grid @visx/group @visx/heatmap @visx/pattern @visx/responsive @visx/scale @visx/shape d3-array @number-flow/react`.
32
+
33
+ **Props:** `config` (what the chart means — `version`, `family`, `title`, `format`, `overlays`, `encode`, and `x` on cartesian families), `theme` (how it is presented — `context`, `motion`, `color`, `chrome`, `interaction`, `labels`), `data` (typed by `config.family`), `loading`, `onHoverChange`, `onRangeChange`, `pinnedIndex` / `defaultPinnedIndex` / `onPinnedIndexChange`, `onValidationIssue`, `replayNonce` (bump to replay enter motion), plus `ref`, `className` and any div prop, landing on the one root in every state.
34
+
35
+ - **Validation is internal and always on** — `withDefaultVersion` → `migrateSpec` → `validateChartSpec` → `diagnoseChart` → `normalize` on every render. An invalid spec never throws and never renders blank: it renders `ChartErrorCard` (`errors`, `labels`) naming the first violation's JSON Pointer. Warnings render the chart and report through `onValidationIssue`, once per distinct set.
36
+ - **`version` is `CURRENT_VERSION` (2).** A stored spec carries its own `version`; only the JSX boundary defaults a missing one. `migrateSpec` walks a spec forward only, so a spec newer than the installed renderer is rejected, never downgraded.
37
+ - **Every colour field takes a token slot, never a literal:** `chart-1`…`chart-5` (categorical, wrapping after five), `chart-positive`, `chart-negative`, `chart-diverging-mid`, `chart-scale-01`…`chart-scale-05`. `theme.color.palette` reorders the five categorical slots; an explicit series `color` wins over it.
38
+ - **Size comes from the parent.** `Chart` sets only `w-full`. The `cell`/`card` backend stretches into its parent's box; the `dashboard` backend renders nothing until it measures a non-zero ancestor — an unsized parent is a silently empty chart either way.
39
+ - **Emit-loop pre-flight:** `validateChartSpec(spec)` returns `{ ok, value | errors }`; `diagnoseChart(spec)` returns `{ errors, warnings }` over the data values (log scale over non-positive values, a palette wrap, date-like x on a category axis) — treat both as blocking before storing an artifact. `explainSpec(spec)` reports which default supplied each resolved value. `normalize` and `withDefaultVersion` are exported beside them.
40
+
41
+ ### Context
42
+
43
+ `theme.context` is `cell`, `card` or `dashboard`; it picks density defaults, the capability envelope and the backend. An omitted context resolves from the host — `cell` inside a `data-mode="inline"` or `data-density="compact"` ancestor, otherwise `card`. **A host never implies `dashboard`**, so a dashboard-only family needs `theme: { context: "dashboard" }` or it fails validation with the recorded reason.
24
44
 
25
- Import via `@nurix/components/<subpath>`; each ships a matching skeleton.
45
+ | Context | Chrome | Interaction | Motion (`auto`) | Families admitted |
46
+ | --- | --- | --- | --- | --- |
47
+ | `cell` | none | hover off unless turned on | none, whatever the request | line, area, bar, pie, progress, ring, gauge, stat |
48
+ | `card` | value labels and legend, off by default | hover on, pin off | enter | line, area, bar, pie, progress, ring, gauge, stat |
49
+ | `dashboard` | grid horizontal and both axes on; value labels and legend admissible | hover with a crosshair; `range` opt-in | full | line, area, bar, composed, scatter, funnel, heatmap-calendar, heatmap-matrix, radar |
26
50
 
27
- - `bar-graph` — `BarGraph`, `BarGraphSkeleton`
28
- - `line-graph` — `LineGraph`, `LineGraphSkeleton` (re-exports `ChartSeries`)
29
- - `pie-graph` — `PieGraph`, `PieGraphSkeleton` (variants pie|half-pie|doughnut|half-doughnut; doughnut variants take a `center` prop that auto-fits content in the hole)
30
- - `progress-bar` — `ProgressBar`, `ProgressBarSkeleton` (segmented multi-series bars; distinct from the `progress` subpath's shadcn Progress primitive)
31
- - `sparkline` — `Sparkline` (legacy; prefer `Spark` for new work)
51
+ ### Families and their data
32
52
 
33
- ## Standalone chart families
53
+ `config.family` is a closed enum, and each family takes exactly one data class. The cartesian families (`line`, `area`, `bar`, `composed`) require `x: { type: "time" | "category" | "linear", grain? }`; `scatter` requires `x: { type: "linear" | "log" | "time", grain? }`. `grain` is `hour`, `day`, `week`, `month`, `quarter` or `year`. `encode` is the one family-shaped key; an unknown key is a rejection naming the legal keys. Shared knobs keep one shape: `sort` is `"none" | "asc" | "desc"` or `{ by, order }`, `stack` is `"none" | "stacked" | "percent"`, `diverging` is `{ baseline?, positiveColor?, negativeColor? }`.
34
54
 
35
- Each on its own subpath with tooltip/legend/skeleton companions and full types:
55
+ Series data is `{ series: [{ id, label, points: [{ x, y, size? }], color? }] }` — `y` may be `null`, `id` is unique and stable, series order is stacking and draw order.
36
56
 
37
- - `funnel` — `Funnel` (+ `FunnelTooltip`, `FunnelLegend`, `FunnelSkeleton`): ordered stage bands. **Props:** `data: FunnelData`, `orientation`, `variant` (default `halo`), `sort` (`asc` renders a pyramid), `showPercentage`/`showValues`/`showLabels`, `showDrop` (inter-stage drop-off annotation), `showTooltip`, `percentBasis` (default `first`), `color`, `layers`
38
- - `gauge` — `Gauge`, `Bullet`, `GaugeSkeleton`: value on a [min,max] scale. **Props:** `orientation` (arc or horizontal notch track), `value` (null renders the empty state), `min`/`max`, `zones` (threshold color bands), `target` (tick marker), `ticks`, `showTicks`/`showValues`, `valueFormat`, `centerContent` (arc only), `totalNotches`
39
- - `heatmap` — `Heatmap`, `HeatmapSkeleton`, `HeatmapLegend`, `HeatmapTooltip`, `HeatmapXAxis`/`HeatmapYAxis`/`HeatmapSeparator`: calendar contribution grid. **Props:** `data: HeatmapColumn[]`, `xDomain`, `layout` [fluid|fill], `binSize`, `gap`, `cornerRadius`, `levelColors` (defaults to the `--chart-scale-*` tokens)
40
- - `heatmap` (same subpath) — `MatrixHeatmap`, `MatrixHeatmapSkeleton`, `MatrixHeatmapLegend`, `MatrixHeatmapTooltip`: x-by-y cell matrix (`MatrixHeatmapData` dense or cell list), quantized onto the token ramp
41
- - `radar` — `Radar`, `RadarSkeleton`: series polygons over spider axes. **Props:** `data` (`RadarSeries[]` or `ChartSeries[]`), `metrics` (ordered axes; derived when omitted), `levels`, `scaling` [independent|shared] (default `independent` — honest mixed-unit rendering), `maxValue`, `nullMode` [zero|gap], `colors`, `size`
42
- - `ring` — `Ring`, `RingSkeleton`: concentric segmented progress. **Props:** `data: RingDatum[]` (innermost first), `strokeWidth`, `ringGap`, `startAngle`/`endAngle`, `lineCap`, `trackColor`, `animated`, `interactive`, `size`
57
+ - **`line`** — multi-series lines. `encode`: `curve` [linear|smooth|step|stepAfter|stepBefore], `nulls` [gap|bridge] (default `gap`), `markers`, `diverging`, `endpointLabels`.
58
+ - **`area`** — filled series. `encode`: `curve`, `nulls` [gap|bridge], `markers`, `diverging`, `stack`, `band: { lower, upper }` (series ids drawing a range band).
59
+ - **`bar`** — grouped or stacked bars. `encode`: `mode` [categorical|histogram|pareto], `stack`, `horizontal`, `sort` (by `value`, `label` or a series id), `diverging`, `bins`, `binWidth`. **Histogram:** `mode: "histogram"` bins each point's raw `y` samples, so already-binned counts are binned again; `stack` and `sort` are rejected. **Pareto:** fixed descending with a cumulative-percent line; `sort` is rejected.
60
+ - **`composed`** (dashboard) — mixed marks over one shared scale pair. `encode.series` maps each series id to `{ mark: "line" | "area" | "bar", axis?: "left" | "right" }`, at most two y axes.
61
+ - **`scatter`** (dashboard) — x/y point field. `encode`: `yScale` [linear|log], `bubble` (reads `size`), `colorByValue`, `trend` (least-squares line), `quadrant: { x?, y?, shade? }`.
62
+ - **`radar`** (dashboard) — series polygons over spider axes; spokes are the points' categorical `x`. `encode`: `scaling` [independent|shared], `maxValue`, `nulls` [gap|zero] (default `gap`).
63
+ - **`pie`** (cell, card) — data `{ parts: [{ label, value, color? }] }`. `encode`: `donut`, `half`, `sort`, `maxSlices`, `otherThreshold`, `otherLabel` (Other sorts last), `minAngle`, `center` [total|none].
64
+ - **`progress`** (cell, card) — stacked horizontal segments from `parts`, left to right; no `encode` keys; the goal tick is a `target` overlay.
65
+ - **`funnel`** (dashboard) — ordered stage bands from `parts`, top-down. `encode`: `variant` [halo|solid] (`halo` draws concentric rings at ramping opacity), `colorMode` [single|ramp|categorical], `sort` (`asc` renders a pyramid), `showValue`, `showPercent`, `showDrop`.
66
+ - **`gauge`** (cell, card) — data `{ value, min, max }`, the domain required. `encode`: `shape` [arc|linear|bullet], `fill` [solid|gradient|scale]. Threshold bands are `zone` overlays and the goal tick a `target` overlay — `shape: "bullet"` with zones and a target is the bullet chart. Hover is unavailable (one always-visible scalar).
67
+ - **`ring`** (cell, card) — concentric value-vs-max rings. Data `{ rings: [{ label, value, max, color? }] }`, innermost first. `encode`: `endAngle` (narrow it for an arc-gauge look).
68
+ - **`heatmap-calendar`** (dashboard) — contribution grid. Data `{ cells: [{ date, value }] }` with ISO dates. `encode`: `weekStartDay` (0–6), `levels`, `hideGhostCells`.
69
+ - **`heatmap-matrix`** (dashboard) — x-by-y cell matrix on the token ramp. Data `{ cells: [{ x, y, value }] }`. `encode`: `colorScaleType` [sequential|diverging], `divergingMidpoint`, `nulls` [empty|zero|hatch] (default `empty`), `xOrder` / `yOrder` (`none`, `asc`, `desc`, `{ by, order }` or an explicit list), `aggregate` [sum|mean|max|min|count], `highlight` [none|cell|cross], `showValues`.
70
+ - **`stat`** (cell, card) — a KPI. Data `{ value, delta?, history? }` (`history` is points). `encode.visual: { family: "line" | "bar" | "pie" | "gauge" | "ring" | "progress" }` draws a cell-scale mini-chart from stat's own data. A KPI laid out as a page unit is `Stat` (`element/stat`, see [layout](layout.md)).
43
71
 
44
- ## Chart chrome primitives
72
+ ```tsx
73
+ import { Chart } from "@nurix/components/block/chart";
74
+
75
+ <Chart
76
+ config={{
77
+ version: 2,
78
+ family: "line",
79
+ title: "Revenue",
80
+ x: { type: "time", grain: "month" },
81
+ format: { y: { kind: "currency", currency: "USD", compact: true } },
82
+ encode: { curve: "smooth" },
83
+ overlays: [{ type: "line", y: 50000, label: "Target", color: "chart-negative" }],
84
+ }}
85
+ theme={{ context: "dashboard", chrome: { legend: "bottom" } }}
86
+ data={{ series: [{ id: "rev", label: "Revenue", points: [{ x: "2026-01-01", y: 42000 }, { x: "2026-02-01", y: 51000 }] }] }}
87
+ />
88
+
89
+ <Chart
90
+ config={{
91
+ version: 2,
92
+ family: "gauge",
93
+ title: "Quota",
94
+ encode: { shape: "bullet" },
95
+ overlays: [{ type: "zone", from: 0, to: 60, color: "chart-scale-01" }, { type: "target", value: 80 }],
96
+ }}
97
+ data={{ value: 72, min: 0, max: 100 }}
98
+ />
99
+ ```
100
+
101
+ ### Chrome, interaction, overlays and labels
45
102
 
46
- `@nurix/components/chart-primitives` — shared tier-1 chrome: `ChartEmpty` (empty state), `ChartHoverCard` (tooltip surface; `ChartHoverCardRow`), `ChartLegend` (`ChartLegendItem`, `ChartLegendPosition`).
103
+ The shared chart chrome — legend, tooltip, empty and error states — is set by the spec, never composed as children.
47
104
 
48
- ## Dashboard charts (`charts` subpath)
105
+ - **`theme.chrome`:** `grid` [none|horizontal|both], `xAxis` / `yAxis` / `yAxisRight` (`{ ticks, tickMode, format, largeNumbers, orientation }` or `false`), `legend` [none|right|bottom|inline], `valueLabels` (`true` or `{ position: "inside" | "outside", endpointOnly, total }`), `background` (a plot-area pattern). `legend` and `valueLabels` are illegal at `cell`; grid, axes and `background` are dashboard-only.
106
+ - **`theme.interaction`:** `hover`, `pin`, `range` [none|xband|box]. `pin` is legal on bar, pie, scatter, funnel and both heatmaps; `xband` on line, area, bar and composed; `box` on scatter only. Not every handler reaches the host from every renderer — dashboard hover reaches `onHoverChange` only from the two heatmaps, pin reaches `onPinnedIndexChange` only from `funnel`, and no renderer emits `onRangeChange`.
107
+ - **`config.overlays`:** `zone` (`from`, `to`, `color`, `label`), `target` (`value`), `line` (`x` or `y`), `dot` (`{ x, y }` or `{ resolve: "max" | "min" | "average", seriesId }`), `annotation` (`x`, `text`, `variant` [point|event]), `projection` (`seriesId`, `horizon`). Each is legal only on the families its envelope lists — `zone` on the cartesian families, scatter and gauge; `target` on line, area, bar, composed, gauge, progress and ring; `projection` on line and area.
108
+ - **`config.format`:** `x`, `y`, `value` channels, each `{ kind: "compact" | "int" | "percent" | "currency" | "unit" | "date", … }` with optional `prefix` / `suffix`.
109
+ - **`theme.motion`:** [none|enter|full|auto] (default `auto`, resolved by context).
110
+ - **`theme.labels`:** every string the chart writes — `empty` ("No data"), `invalid`, `invalidName`, `more`, `didYouMean`, `total`, `loading`, `retry`.
49
111
 
50
- Composable visx-based shells — a shell provides scales/context, marks draw data ink, chrome and overlays compose as children. All from `@nurix/components/charts`.
112
+ ## StatusTimeline
51
113
 
52
- **Shells:** `AreaChart`, `BarChart` (`BarOrientation`), `LineChart`, `ScatterChart`, `ComposedChart` — plus standalone loading shells `AreaChartLoading`, `BarChartLoading`, `LineChartLoading`
114
+ An uptime/status strip: one stacked bar per day with a shared day card on hover, focus or pin. No axis — a quantitative time series is `Chart`, a single percentage is `element/progress`. Import `StatusTimeline` and `formatDateRange` from `@nurix/components/compound/status-timeline`.
53
115
 
54
- **Recipes & transforms:** `Bullet`, `Histogram` (+ `computeHistogramBins`, `binsToRows`), `ParetoLine` (+ `computeParetoRows`), `BarSort`, `BarStackMode`
116
+ **Props:** `data: StatusTimelineData[]`, `title` (a muted caption that also names the strip), `height` (px, default `50`), `spacing` [none|sm|md|lg] (default `sm`), `roundedCorners` [none|top|all] (default `top`), `isNormalized` (default `true`), `hasTrack`, `animate` (default `false`), `locale`, `labels`, `loading`, `emptyState`, `renderBar(segment, index, { day, total })`, `renderCard`, `renderEvent`, `render`
55
117
 
56
- **Marks:** `Area`, `RangeArea`, `Bar`, `Line` (`DivergingConfig`), `Scatter`, `SeriesBar`, `SeriesMarkers`, `ValueLabels`; curve factories `curveLinear`, `curveMonotoneX`, `curveNatural`, `curveStep`, `curveStepAfter`, `curveStepBefore` (pass to a mark's `curve` prop — never import `@visx/curve` directly)
118
+ - One record per day: `{ day, summary?, bar: [{ intent, height }], card: [{ intent, value }], events: [{ id, name, type, from, to }] }`. `day` is an ISO date read in local time; `summary` is the card's muted header line; event `type` is `incident`, `report` or `maintenance`.
119
+ - `isNormalized` reads `bar[].height` as a percentage of 100 per day — pass raw counts with `isNormalized={false}`, or every bar mis-fills.
120
+ - Colour comes from `intent` (`neutral`, `info`, `success`, `warning`, `danger`) and the `--intent-*` tokens; retint by redeclaring the tokens, never a per-strip colour map.
121
+ - `loading` renders the skeleton; there is no separate skeleton member.
122
+ - `formatDateRange(from?, to?, { locale, labels })` formats an event's range, collapsing a same-day range.
57
123
 
58
- **Chrome:** `XAxis`, `YAxis`, `BarXAxis`, `BarYAxis`, `Grid`, `Background` (pattern presets), `PatternArea`, `PatternCircles`/`PatternHexagons`/`PatternLines`/`PatternWaves`
124
+ ```tsx
125
+ <StatusTimeline
126
+ title="API uptime"
127
+ data={[{
128
+ day: "2026-08-07",
129
+ summary: "1,204 requests",
130
+ bar: [{ intent: "success", height: 92 }, { intent: "warning", height: 8 }],
131
+ card: [{ intent: "success", value: "92%" }, { intent: "warning", value: "8%" }],
132
+ events: [],
133
+ }]}
134
+ />
135
+ ```
59
136
 
60
- **Tooltip & legend:** composable subsystems re-exported whole (`./tooltip`, `./legend` internals)
137
+ ## Chart kernel
61
138
 
62
- **Overlays:** `ReferenceLine`, `ReferenceArea`, `ReferenceDot`, `Annotation` (`AnnotationVariant`), `ProjectionLine`, `ProfitLossLine` + `ProfitLossLegend`, `ChartBrush`
139
+ The coherence-bearing subset of the chart core, for bespoke visualization code that must match the `Chart` system. Reach for it only for a mark neither `Chart` nor `Spark` draws; needing decimation, scatter geometry, thresholds or interaction scheduling is the signal to use a chart family — those stay unpublished.
63
140
 
64
- **Data contract:** `ChartPoint`, `ChartSeries`, `isChartSeriesArray`, `seriesToRows`
141
+ - **`lib/chart-kernel`:** the data contract types `ChartSeries` (`{ id, label, points, color? }`), `ChartPoint` (`{ x, y }`), `ChartSlice`, `ColorableSeries`; the colour policy `resolveSeriesColors(series, { useTokens })` / `resolveSeriesColor(index, explicitColor?, { useTokens })`; `buildColorScale(min, max, kind)` (`sequential` by default over `--chart-scale-01..05`, or `diverging` over `--chart-negative` / `--chart-diverging-mid` / `--chart-positive`); the formatters `formatCompactNumber`, `formatPercent(ratio, precision)`, `intFmt`, `shortDateFmt`, `weekdayDateFmt`, `monthLongFmt`, `monthShortFmt`, `weekdayLongFmt`, `hmsTimeFmt`; and `isChartDataEmpty(series)`.
142
+ - **`lib/chart-kernel/colors`:** `resolveSeriesColors`, `resolveSeriesColor` at a narrower entry.
143
+ - **`lib/chart-kernel/arc`:** `computeSliceAngles(values, startAngle, totalAngle)`, `arcCentroid`, `buildArcPath({ cx, cy, innerRadius, outerRadius, startAngle, endAngle, padAngle?, cornerRadius? })` (wedge and ring paths), `isFullArc(startAngle, endAngle)`.
144
+ - **`lib/chart-kernel/line`:** `resolveLineDomain(values)`, `createLineScales(config)`, `buildPolylinePoints`, `buildAreaPoints`, `buildLinePath(points, scales, curve, connectNulls)`, `buildAreaPath`, `buildBandAreaPath` — `curve` is `linear`, `smooth`, `step`, `stepAfter` or `stepBefore`.
145
+ - Colour resolves explicit override → `var(--chart-1..5)` → self-contained `oklch` fallbacks; pass `{ useTokens: false }` only for a shell with no theme wiring, or the marks stop re-branding with the host stylesheet.
146
+ - The formatter singletons are fixed to `en-US` at module scope and never follow the runtime locale — a localized axis builds its own `Intl` instance.
65
147
 
66
148
  ```tsx
67
- import { LineChart, Line, XAxis, YAxis, Grid, ReferenceLine } from "@nurix/components/charts";
68
-
69
- <LineChart series={revenue}>
70
- <Grid />
71
- <XAxis />
72
- <YAxis />
73
- <Line curve={curveMonotoneX} />
74
- <ReferenceLine y={target} label="Target" />
75
- </LineChart>
149
+ import { resolveSeriesColors } from "@nurix/components/lib/chart-kernel";
150
+ import { computeSliceAngles, buildArcPath } from "@nurix/components/lib/chart-kernel/arc";
151
+
152
+ const colors = resolveSeriesColors(slices);
153
+ const angles = computeSliceAngles(slices.map((s) => s.value), 0, Math.PI * 2);
76
154
  ```