@nurix/etna 0.18.18 → 0.18.20

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 (100) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +2 -2
  3. package/data/harnesses/code/rules/general.md +2 -1
  4. package/data/harnesses/code/skills/design/SKILL.md +65 -0
  5. package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
  6. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
  7. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
  8. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
  9. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
  10. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
  11. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
  12. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
  13. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
  14. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
  15. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
  16. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
  17. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
  18. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
  19. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
  20. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
  21. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
  22. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
  23. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
  24. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
  25. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
  26. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
  27. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
  28. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
  29. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
  30. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
  31. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
  32. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
  33. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
  34. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
  35. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
  36. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
  37. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
  38. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
  39. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
  40. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
  41. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
  42. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
  43. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
  44. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
  45. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
  46. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
  47. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
  48. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
  49. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
  50. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
  51. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
  52. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
  53. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
  54. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
  55. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
  56. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
  57. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
  58. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
  59. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
  60. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
  61. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
  62. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
  63. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
  64. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
  65. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
  66. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
  67. package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
  68. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
  69. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
  70. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
  71. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
  72. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
  73. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
  74. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
  75. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
  76. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
  77. package/data/harnesses/code/skills/design/references/create.md +126 -0
  78. package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
  79. package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
  80. package/data/harnesses/code/skills/design/references/improve.md +85 -0
  81. package/data/harnesses/code/skills/design/references/interaction.md +77 -0
  82. package/data/harnesses/code/skills/design/references/introspect.md +100 -0
  83. package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
  84. package/data/harnesses/code/skills/design/references/personality.md +504 -0
  85. package/data/harnesses/code/skills/design/references/structure.md +138 -0
  86. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
  87. package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
  88. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
  89. package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
  90. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
  91. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
  92. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
  93. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
  94. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
  95. package/data/harnesses/etna/CLAUDE.md +1 -1
  96. package/data/harnesses/etna/harness.json +2 -2
  97. package/data/harnesses/etna/rules/general.md +2 -1
  98. package/data/harnesses/mobile/harness.json +2 -2
  99. package/data/harnesses/mobile/rules/general.md +2 -1
  100. package/package.json +1 -1
@@ -0,0 +1,504 @@
1
+ # Personality — the look pipeline
2
+
3
+ `design.md → derivation → token emission`. One resolved `design.md` (its two machine blocks — see
4
+ [SCHEMA.md](../data/design-md/SCHEMA.md)) becomes three emitted files that *are* the state: no sidecar
5
+ cache, no JSON. `derive-personality.mjs` runs the pipeline: `--check` is the coverage gate that must
6
+ pass before `--emit` writes `personality.css`, `globals.css`, and the `<html>` attributes in `index.html`.
7
+
8
+ The look comes entirely from the cascade. A surface composes in semantic tokens (`bg-card`,
9
+ `text-foreground`) and reads its personality from the CSS variables this file emits. Switch the resolved
10
+ `design.md`, re-emit, and every surface re-skins with zero JSX edits — the invariant the exemplars are
11
+ validated against.
12
+
13
+ ## 1. The pipeline
14
+
15
+ ```
16
+ resolved design.md ──▶ derive-personality.mjs --check ──▶ (green) ──▶ --emit src/styles/
17
+ (design-primitives coverage gate: personality.css
18
+ + lever-manifest) every primitive, dial, token globals.css
19
+ assigned or hinted, or exit 1 index.html <html> attrs
20
+ ```
21
+
22
+ `--check` never best-efforts (D6/D29): if any of the 26 primitives, 2 dials, density tier, or 34 color
23
+ tokens is unassigned and un-hinted it exits non-zero naming the offending keys, and the `tokens` phase
24
+ gate stays shut. Emission runs only over a document that passed.
25
+
26
+ ## 2. Derivation — design.md → primitives and tokens
27
+
28
+ Hybrid: **deterministic wherever the document states a value; judgment-with-recorded-rationale for the
29
+ gaps.** The `design-primitives` block already carries all 26 keys (names without the leading `--`) plus
30
+ the dials, so the mapping is near 1:1.
31
+
32
+ | design.md source | Emitted target | Rule |
33
+ | --- | --- | --- |
34
+ | the 24 static primitives (geometric · typographic · surface · iconography · layout · temporal) | `[data-style="<slug>"] { --<key>: <value> }` | 1:1 by name |
35
+ | `density-gap`, `density-inset` | governed refs in `[data-style]`; base set by the tier | via the density mechanism (§7) |
36
+ | `ease-enter`, `ease-exit`, `stagger-delay` (derived — *not* in the 26) | computed at emit | rules below |
37
+ | `lever-manifest.color-tokens.light.<token>` (17 semantic + 17 substrate) | `:root, :root[data-theme="light"] { --<token>: <value> }` | 1:1 by name |
38
+ | `verbosity` · `feedback` | `<html data-verbosity>` · `<html data-feedback>` | dial → attribute |
39
+ | `density-tier` | `<html data-density>` → `[data-density]` base | tier → attribute |
40
+ | `fonts.sans` / `fonts.mono` (equal `font-sans`/`font-mono`) | `@theme inline` font keys + the primitive | §4 |
41
+ | `product-traits` | read by the front gate; **not** emitted to CSS | — |
42
+
43
+ **Derived temporal** (SCHEMA: computed from `ease-default` and `duration-unit`, never authored, never
44
+ gated):
45
+ - `--ease-exit: var(--ease-default)` — departure mirrors the neutral curve.
46
+ - `--ease-enter` → the emphasized-decelerate arrival curve `cubic-bezier(0.16, 1, 0.3, 1)` (fast in,
47
+ settles clean); reuse `ease-default` verbatim if it is already a decelerate curve.
48
+ - `--stagger-delay` → `duration-unit × 0.15`, clamped `0–40ms`, emitted as a concrete ms; a snappier
49
+ tempo staggers less. (`50ms → 8ms`.)
50
+
51
+ **Gaps → hint → write-back.** Where a value needs judgment the document carries a `hint:` line. A hint
52
+ passes `--check` but not `--emit`: emission resolves each hint to a concrete value *and rewrites the line
53
+ with its rationale* so the document stays self-explaining and re-derivable (`contrast-boost: 1.1 #
54
+ derived: dense-grid brief → one step up (was hint)`). Judgment is recorded into the `design.md`, never
55
+ silent — the full convention is [SCHEMA.md](../data/design-md/SCHEMA.md) §"hint convention". After
56
+ resolution no bare `hint:` remains.
57
+
58
+ ## 3. `src/styles/personality.css`
59
+
60
+ The token system. The header comment carries the resolved personality — this comment *is* the cache
61
+ record. Values below are one project's (`meridian`, the SCHEMA worked example); the shape is invariant.
62
+
63
+ ```css
64
+ /* src/styles/personality.css — operator-UI personality tokens (emitted; do not hand-edit).
65
+ *
66
+ * Active personality — code is the cache. There is no JSON sidecar; these values
67
+ * and the <html data-*> attributes in index.html ARE the state. To change the look,
68
+ * edit the resolved design.md and re-run derive-personality.mjs --emit.
69
+ * style: meridian (<html data-style="meridian">) — the project's own personality (rosterless)
70
+ * density: dense (<html data-density="dense">)
71
+ * verbosity: default (<html data-verbosity="default">)
72
+ * feedback: subtle (<html data-feedback="subtle">)
73
+ * theme: light (<html data-theme="light">) — light is the default; dark is opt-in
74
+ *
75
+ * Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
76
+ * this file resolves those names to values. Components never use dark: prefixes or literal colors.
77
+ */
78
+
79
+ /* ── System baseline: the 26 primitives at neutral operator defaults. ──
80
+ * The safety net if <html> ever carries no data-style. The project's real
81
+ * values live in the single [data-style="meridian"] block below, which
82
+ * overrides these. Improve/extract rewrites that block, never this baseline. */
83
+ :root {
84
+ /* geometric */
85
+ --radius: 6px;
86
+ --shadow-intensity: 1;
87
+ --border-weight: 1px;
88
+ --density-base-gap: 1; /* the density tier writes this; see the density mechanism */
89
+ --density-base-inset: 1;
90
+ --density-gap: var(--density-base-gap);
91
+ --density-inset: var(--density-base-inset);
92
+ /* typographic */
93
+ --font-sans: "Inter", system-ui, sans-serif;
94
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
95
+ --font-weight-normal: 400;
96
+ --font-weight-strong: 600;
97
+ --heading-tracking: -0.01em;
98
+ /* temporal (+ derived) */
99
+ --duration-unit: 200ms;
100
+ --duration-fast: calc(var(--duration-unit) * 0.5);
101
+ --duration-normal: var(--duration-unit);
102
+ --duration-slow: calc(var(--duration-unit) * 2);
103
+ --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
104
+ --ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
105
+ --ease-exit: var(--ease-default);
106
+ --stagger-delay: 30ms;
107
+ /* surface */
108
+ --contrast-boost: 1;
109
+ /* iconography */
110
+ --icon-stroke: 1.75;
111
+ --icon-size-base: 16px;
112
+ /* operator layout */
113
+ --sidebar-width-expanded: 240px;
114
+ --sidebar-width-collapsed: 56px;
115
+ --topbar-height: 52px;
116
+ --side-panel-width: 480px;
117
+ --side-panel-width-rich: 640px;
118
+ --modal-width-max: 480px;
119
+ --form-max-width: 720px;
120
+ --table-row-height: 40px;
121
+ /* behavioral fallbacks (match verbosity=default + feedback=subtle) */
122
+ --label-display: inline-flex;
123
+ --helper-text-display: block;
124
+ --tooltip-delay: 500ms;
125
+ --feedback-press-scale: 0.99;
126
+ --focus-ring-width: 2px;
127
+ --focus-ring-offset: 1px;
128
+ }
129
+
130
+ /* ── Semantic + substrate color tokens — light theme. ──
131
+ * CASCADE CONTRACT 1 (§6): the doubled selector out-specifies the library's bare
132
+ * :root defaults in @nurix/components/styles.css — but ONLY because <html> carries
133
+ * data-theme="light". A bare :root here loses to the later library import. All 34
134
+ * tokens are set (D6): no consumed token falls back to a shadcn-gray default. */
135
+ :root,
136
+ :root[data-theme="light"] {
137
+ /* semantic (17) */
138
+ --background: #ffffff;
139
+ --foreground: #0f172a;
140
+ --card: #ffffff;
141
+ --card-foreground: #0f172a;
142
+ --muted: #f1f5f9;
143
+ --muted-foreground: #64748b;
144
+ --border: #e2e8f0;
145
+ --input: #e2e8f0;
146
+ --ring: #6366f1;
147
+ --primary: #4f46e5;
148
+ --primary-foreground: #ffffff;
149
+ --accent: #eef2ff;
150
+ --accent-foreground: #4f46e5;
151
+ --destructive: #dc2626;
152
+ --destructive-foreground: #ffffff;
153
+ --success: #16a34a;
154
+ --warning: #d97706;
155
+ /* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
156
+ --popover: #ffffff;
157
+ --popover-foreground: #0f172a;
158
+ --secondary: #f1f5f9;
159
+ --secondary-foreground: #0f172a;
160
+ --chart-1: #4f46e5;
161
+ --chart-2: #0ea5e9;
162
+ --chart-3: #16a34a;
163
+ --chart-4: #d97706;
164
+ --chart-5: #dc2626;
165
+ --sidebar: #f8fafc;
166
+ --sidebar-foreground: #0f172a;
167
+ --sidebar-primary: #4f46e5;
168
+ --sidebar-primary-foreground: #ffffff;
169
+ --sidebar-accent: #eef2ff;
170
+ --sidebar-accent-foreground: #4f46e5;
171
+ --sidebar-border: #e2e8f0;
172
+ --sidebar-ring: #6366f1;
173
+ }
174
+
175
+ /* ── The project personality: the single [data-style] block (D3, rosterless). ──
176
+ * Named after the project — not a preset. This materializes the design.md
177
+ * design-primitives block: all 26 primitives + the derived temporal. It is the
178
+ * one block improve/extract rewrites. density-gap/inset are governed refs (§7),
179
+ * so the global tier and per-surface step still compose. */
180
+ [data-style="meridian"] {
181
+ --radius: 6px;
182
+ --shadow-intensity: 0.5;
183
+ --border-weight: 1px;
184
+ --density-gap: var(--density-base-gap);
185
+ --density-inset: var(--density-base-inset);
186
+ --font-sans: "Inter", system-ui, sans-serif;
187
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
188
+ --font-weight-normal: 400;
189
+ --font-weight-strong: 600;
190
+ --heading-tracking: -0.01em;
191
+ --duration-unit: 50ms;
192
+ --duration-fast: 100ms;
193
+ --duration-normal: 150ms;
194
+ --duration-slow: 250ms;
195
+ --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
196
+ --ease-enter: cubic-bezier(0.16, 1, 0.3, 1); /* derived from ease-default */
197
+ --ease-exit: var(--ease-default); /* derived: departure mirrors the neutral curve */
198
+ --stagger-delay: 8ms; /* derived: duration-unit × 0.15, clamped 0–40ms */
199
+ --contrast-boost: 1;
200
+ --icon-stroke: 1.75;
201
+ --icon-size-base: 16px;
202
+ --sidebar-width-expanded: 248px;
203
+ --sidebar-width-collapsed: 56px;
204
+ --topbar-height: 52px;
205
+ --side-panel-width: 420px;
206
+ --side-panel-width-rich: 640px;
207
+ --modal-width-max: 560px;
208
+ --form-max-width: 640px;
209
+ --table-row-height: 40px;
210
+ }
211
+
212
+ /* ── Behavioral dials (D4) — orthogonal to the visual primitives. ── */
213
+ [data-verbosity="full"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 800ms; }
214
+ [data-verbosity="default"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 500ms; }
215
+ [data-verbosity="compact"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 200ms; }
216
+ [data-verbosity="expert"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 0ms; }
217
+
218
+ [data-feedback="subtle"] { --feedback-press-scale: 0.99; --focus-ring-width: 2px; --focus-ring-offset: 1px; }
219
+ [data-feedback="minimal"] { --feedback-press-scale: 1; --focus-ring-width: 1px; --focus-ring-offset: 0px; }
220
+
221
+ /* ── Density: global tier → base multipliers (D31). ──
222
+ * The tier writes ONLY the base; --density-gap/inset resolve from it (above),
223
+ * so a per-surface step can re-derive relative to the tier. See §7. */
224
+ [data-density="dense"] { --density-base-gap: 0.85; --density-base-inset: 0.85; }
225
+ [data-density="standard"] { --density-base-gap: 1; --density-base-inset: 1; }
226
+ [data-density="spacious"] { --density-base-gap: 1.15; --density-base-inset: 1.15; }
227
+
228
+ /* ── Per-archetype density modifier (D31) — relative step on the tier, pure cascade, no JS. ──
229
+ * A surface element carries data-density-step; it re-derives one step off the
230
+ * inherited base (a log-stream runs tighter than a settings form inside the same
231
+ * app). Reads --density-base-* (never itself) so it composes with the tier and
232
+ * never absolutely overrides it. The recomputed --density-gap flows into the
233
+ * library's p-*/gap-* through the @theme --spacing remap (§4). */
234
+ [data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
235
+ [data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
236
+
237
+ /* ── Dark theme — OPT-IN ONLY. ──
238
+ * Emitted only when the design.md ships a lever-manifest color-tokens.dark map.
239
+ * Light stays the default; this block never appears for a light-only project.
240
+ [data-theme="dark"] {
241
+ --background: #0f172a; --foreground: #f8fafc; ... all 34 tokens remapped ...
242
+ } */
243
+
244
+ /* ── Accessibility ── */
245
+ @media (prefers-reduced-motion: reduce) {
246
+ :root, [data-style] {
247
+ --duration-unit: 0ms; --stagger-delay: 0ms;
248
+ --ease-default: linear; --ease-enter: linear; --ease-exit: linear;
249
+ --feedback-press-scale: 1;
250
+ }
251
+ }
252
+ @media (prefers-contrast: more) {
253
+ :root, [data-style] {
254
+ --contrast-boost: 1.15; --border-weight: 2px; --shadow-intensity: 0; --icon-stroke: 2;
255
+ }
256
+ }
257
+ ```
258
+
259
+ ## 4. `src/styles/globals.css`
260
+
261
+ Wires personality into Tailwind v4. `main.tsx` imports this one file. The `@theme inline` keyword inlines
262
+ each value into the generated utilities (so `bg-card` compiles to `background-color: var(--card)` and
263
+ `--font-sans: var(--font-sans)` is *not* a circular `:root` write — it inlines into `.font-sans`).
264
+
265
+ ```css
266
+ /* src/styles/globals.css — Tailwind v4 glue + @theme inline mappings (emitted). */
267
+
268
+ /* CASCADE CONTRACT 2 (§6): this four-step order is LOAD-BEARING. The library's
269
+ * styles.css ships its own :root token defaults and its own @theme radius scale;
270
+ * our @theme MUST come AFTER the import to win the radius scale and own every
271
+ * mapping. rules/general.md (frontend section) states the same order — co-authority. */
272
+ @import "./personality.css"; /* 1. token source (must be first) */
273
+ @import "tailwindcss"; /* 2. framework */
274
+ @import "@nurix/components/styles.css"; /* 3. library (before our @theme) */
275
+
276
+ @theme inline { /* 4. consumer theme — AFTER the library, so it wins */
277
+ /* radius scale — shadcn offsets on --radius, re-asserted so the CONSUMER owns it.
278
+ * A flat personality sets --radius: 0 → every step collapses to 0. */
279
+ --radius-sm: calc(var(--radius) - 4px);
280
+ --radius-md: calc(var(--radius) - 2px);
281
+ --radius-lg: var(--radius);
282
+ --radius-xl: calc(var(--radius) + 4px);
283
+
284
+ /* density remap — the single line that scales ALL library spacing. Every p-*/gap-*
285
+ * is calc(var(--spacing) × N); driving --spacing off --density-gap flows the tier
286
+ * AND the per-surface density step (§7) into the library's internals. */
287
+ --spacing: calc(0.25rem * var(--density-gap));
288
+
289
+ /* shadow — alpha rides --shadow-intensity (matches the library's shadow-xs/sm/md/lg use) */
290
+ --shadow-2xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.04 * var(--shadow-intensity)));
291
+ --shadow-xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.06 * var(--shadow-intensity)));
292
+ --shadow-sm: 0 1px 3px 0 rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 1px 2px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
293
+ --shadow-md: 0 4px 6px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 2px 4px -2px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
294
+ --shadow-lg: 0 10px 15px -3px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 4px 6px -4px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
295
+
296
+ /* font weight — two-weight discipline: medium/semibold/bold all resolve to the
297
+ * single emphasis weight; operator UI carries no third weight. normal is emitted
298
+ * as a literal because its theme key and the primitive share a name (no self-ref). */
299
+ --font-weight-normal: 400;
300
+ --font-weight-medium: var(--font-weight-strong);
301
+ --font-weight-semibold: var(--font-weight-strong);
302
+ --font-weight-bold: var(--font-weight-strong);
303
+
304
+ /* tracking — heading tightness reaches the library's tracking-* utilities */
305
+ --tracking-tighter: var(--heading-tracking);
306
+ --tracking-tight: var(--heading-tracking);
307
+
308
+ /* fonts — emitted as the literal stack: a font is not a runtime-toggled token, and
309
+ * the theme key shares a name with the personality primitive (no self-reference).
310
+ * personality.css carries the same --font-sans/--font-mono for direct var() use. */
311
+ --font-sans: "Inter", system-ui, sans-serif;
312
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
313
+
314
+ /* transitions */
315
+ --transition-fast: var(--duration-fast) var(--ease-default);
316
+ --transition-normal: var(--duration-normal) var(--ease-default);
317
+ --transition-slow: var(--duration-slow) var(--ease-default);
318
+ --transition-enter: var(--duration-normal) var(--ease-enter);
319
+ --transition-exit: var(--duration-fast) var(--ease-exit);
320
+
321
+ /* semantic + substrate color maps — all 34, so bg-popover / bg-chart-1 / bg-sidebar resolve.
322
+ * --color-destructive-foreground is mapped explicitly (D6): the library leaves it unmapped. */
323
+ --color-background: var(--background);
324
+ --color-foreground: var(--foreground);
325
+ --color-card: var(--card);
326
+ --color-card-foreground: var(--card-foreground);
327
+ --color-muted: var(--muted);
328
+ --color-muted-foreground: var(--muted-foreground);
329
+ --color-border: var(--border);
330
+ --color-input: var(--input);
331
+ --color-ring: var(--ring);
332
+ --color-primary: var(--primary);
333
+ --color-primary-foreground: var(--primary-foreground);
334
+ --color-accent: var(--accent);
335
+ --color-accent-foreground: var(--accent-foreground);
336
+ --color-destructive: var(--destructive);
337
+ --color-destructive-foreground: var(--destructive-foreground);
338
+ --color-success: var(--success);
339
+ --color-warning: var(--warning);
340
+ --color-popover: var(--popover);
341
+ --color-popover-foreground: var(--popover-foreground);
342
+ --color-secondary: var(--secondary);
343
+ --color-secondary-foreground: var(--secondary-foreground);
344
+ --color-chart-1: var(--chart-1);
345
+ --color-chart-2: var(--chart-2);
346
+ --color-chart-3: var(--chart-3);
347
+ --color-chart-4: var(--chart-4);
348
+ --color-chart-5: var(--chart-5);
349
+ --color-sidebar: var(--sidebar);
350
+ --color-sidebar-foreground: var(--sidebar-foreground);
351
+ --color-sidebar-primary: var(--sidebar-primary);
352
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
353
+ --color-sidebar-accent: var(--sidebar-accent);
354
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
355
+ --color-sidebar-border: var(--sidebar-border);
356
+ --color-sidebar-ring: var(--sidebar-ring);
357
+ }
358
+
359
+ /* dark variant — emitted ONLY when the project ships dark; redefines the library's
360
+ * class-based dark to follow data-theme so the library's dark: utilities honor our
361
+ * convention. Absent for a light-only project.
362
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); */
363
+ ```
364
+
365
+ ## 5. `index.html`
366
+
367
+ Vite entry (no `layout.tsx`). The `<html>` attributes carry the personality state; the HTML comment is
368
+ the cache record.
369
+
370
+ ```html
371
+ <!-- Personality state lives in these <html data-*> attributes — code is the cache.
372
+ There is no sidecar JSON. To change the look, edit the resolved design.md and
373
+ re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
374
+ <html
375
+ lang="en"
376
+ data-style="meridian"
377
+ data-density="dense"
378
+ data-verbosity="default"
379
+ data-feedback="subtle"
380
+ data-theme="light"
381
+ >
382
+ <head>
383
+ <meta charset="UTF-8" />
384
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
385
+ <title>Meridian</title>
386
+ </head>
387
+ <body>
388
+ <div id="root"></div>
389
+ <script type="module" src="/src/main.tsx"></script>
390
+ </body>
391
+ </html>
392
+ ```
393
+
394
+ ## 6. The two cascade contracts and their failure modes
395
+
396
+ Both are hardened with the in-CSS comments above and enforced by the code-and-token rules (`cod-*`) in
397
+ [hard-rules.md](./hard-rules.md); the `scaffold` and `tokens` phase gates check them.
398
+
399
+ **Contract 1 — the specificity/attribute pair.** Semantic + substrate tokens sit under
400
+ `:root, :root[data-theme="light"]` **and** `<html>` carries `data-theme="light"`.
401
+ *Failure mode:* a bare `:root { --background: … }` is specificity `(0,1,0)` — the library's own
402
+ `:root { --background: … }` in `styles.css` is also `(0,1,0)` and imported *later*, so it wins and every
403
+ surface silently reverts to shadcn gray. The `:root[data-theme="light"]` arm is `(0,2,0)` and out-wins —
404
+ but only if `data-theme="light"` is actually on `<html>`; drop the attribute and the arm never matches,
405
+ leaving only the losing bare `:root`. Both halves are load-bearing together.
406
+
407
+ **Contract 2 — the import order.** `personality.css → tailwindcss → @nurix/components/styles.css →
408
+ @theme inline`. *Failure mode:* put `@theme inline` (or the whole consumer theme) *before* the library
409
+ import and the library's own `@theme` radius scale (`--radius-lg: var(--radius)`, its own offsets) comes
410
+ later and wins — the personality `--radius` stops driving the scale and archetype radius switching breaks.
411
+ The consumer `@theme` must come last. `rules/general.md` (frontend section) states this same order — it is
412
+ co-authority, not a restatement to drift from.
413
+
414
+ ## 7. Density — global tier composed with a per-archetype relative step (D31)
415
+
416
+ Two composed levels, pure cascade, no JS:
417
+
418
+ 1. **Global tier** — `density-tier` (spacious | standard | dense) from the `design.md` emits
419
+ `data-density` on `<html>`. `[data-density="…"]` writes only the **base** multipliers
420
+ (`--density-base-gap/inset`); `--density-gap/inset` resolve *from* the base. Switching the tier
421
+ re-tiers the whole surface coherently.
422
+ 2. **Per-archetype step** — a surface carries `data-density-step="tighter"|"looser"` (from its
423
+ surface-catalog `density-modifier`, [surface-catalog.md](../data/operator-pack/surface-catalog.md)),
424
+ emitted by `scaffold.mjs` onto the surface root. It re-derives `--density-gap/inset` as
425
+ `calc(base × 0.85|1.15)` — a **relative** step off whatever tier the app runs, reading
426
+ `--density-base-*` (never itself, so no circular reference and never an absolute override). A
427
+ log-stream inside a `standard` app resolves one step denser without pinning an absolute value.
428
+
429
+ Both feed the library through `--spacing: calc(0.25rem * var(--density-gap))` in `@theme`: every library
430
+ `p-*`/`gap-*` is `calc(var(--spacing) × N)`, and `--density-gap` resolves per element (respecting a
431
+ `data-density-step` ancestor), so the tier and the step both reach the library's internal spacing. This
432
+ composes *with*, never replaces, the behavioral dials (D4) — density is spatial chrome; verbosity/feedback
433
+ are behavior.
434
+
435
+ ## 8. Dial wiring and the lifecycle
436
+
437
+ `data-verbosity` and `data-feedback` set behavioral variables the lifecycle and interaction layers read:
438
+
439
+ - `--label-display` / `--helper-text-display` — `compact`/`expert` verbosity collapse form labels and
440
+ helper text; `full`/`default` keep them. Field chrome reads these, not a JS branch.
441
+ - `--tooltip-delay` — tightens with verbosity (`800ms → 0ms`).
442
+ - `--feedback-press-scale`, `--focus-ring-width`, `--focus-ring-offset` — `minimal` feedback flattens the
443
+ press and thins the focus ring; `subtle` keeps the operator-default ring.
444
+
445
+ The lifecycle pattern ([lifecycle.md](./lifecycle.md)) consumes the dials directly: empty-state richness
446
+ scales with verbosity (`full` gets the acknowledge-value-action triad, `expert` gets one bare line);
447
+ confirmation depth rides feedback. Focus and overlay discipline read the feedback ring variables
448
+ ([interaction.md](./interaction.md)). Components that must branch structurally on a dial read the `<html>`
449
+ attribute — they never duplicate the state.
450
+
451
+ ## 9. Sidebar width via the SidebarProvider style prop
452
+
453
+ The library hardcodes `--sidebar-width` / `--sidebar-width-icon` as inline styles, overridable only via the
454
+ provider's `style` prop. The shell wires the personality primitives in:
455
+
456
+ ```tsx
457
+ <SidebarProvider
458
+ style={{
459
+ "--sidebar-width": "var(--sidebar-width-expanded)",
460
+ "--sidebar-width-icon": "var(--sidebar-width-collapsed)",
461
+ } as React.CSSProperties}
462
+ >
463
+ ```
464
+
465
+ This is the one layout primitive that cannot reach the library through `@theme` — the style prop is the
466
+ supported seam.
467
+
468
+ ## 10. Coverage checklist — every token the library consumes is emitted
469
+
470
+ Enumerated against [the substrate audit](../../../docs/brainstorm/design/u1-audit-report.md). Emission
471
+ sets each; none falls back to a shadcn-gray default (D6).
472
+
473
+ | Library consumes | How | Emitted at |
474
+ | --- | --- | --- |
475
+ | `--radius` (283 `rounded-*`) | direct primitive + `@theme` radius scale | `[data-style]` · `@theme` |
476
+ | shadow (`shadow-xs/sm/md/lg`, 58) | `--shadow-*` alpha rides `--shadow-intensity` | `@theme` |
477
+ | font weight (`font-medium/semibold/bold/normal`, 139) | two-weight map to `--font-weight-strong` | `@theme` |
478
+ | tracking (`tracking-*`, 13) | `--tracking-tight/tighter → --heading-tracking` | `@theme` |
479
+ | spacing (all `p-*`/`gap-*`) | `--spacing` density remap | `@theme` |
480
+ | `font-mono` (9) / `font-sans` | inlined font keys | `@theme` |
481
+ | 15 semantic colors (888 uses) | direct token values | `:root[data-theme=light]` |
482
+ | `--success`, `--warning` | direct token values (library ignores today — §11) | `:root[data-theme=light]` |
483
+ | `--destructive-foreground` (unmapped by library) | explicit `--color-destructive-foreground` map | `@theme` |
484
+ | 17 substrate (`popover`/`secondary`/`chart-1..5`/`sidebar` family) | direct token values | `:root[data-theme=light]` |
485
+ | `--sidebar-width` / `--sidebar-width-icon` | SidebarProvider `style` prop | shell (§9) |
486
+
487
+ ## 11. Upstream gaps — expected, never worked around
488
+
489
+ Some values live in `@nurix/components` JS, not CSS, so they do not follow tokens until the library adopts
490
+ them. This is expected, filed as user-owned upstream work (D6) — **never** patched with brittle per-instance
491
+ overrides:
492
+
493
+ - **Chart palettes** — the d3 family (`bar-graph`, `line-graph`, `pie-graph`, `sparkline`, …) hardcodes
494
+ oklch/hex and ignores `--chart-1..5`. Color props are per-instance overridable, but the defaults are
495
+ upstream. Emit `--chart-1..5` regardless, so adoption is a drop-in.
496
+ - **Status literals** — `changes-list` and `statistics` hardcode emerald/amber/sky rather than
497
+ `--success`/`--warning`. Upstream token adoption is the fix.
498
+ - **Motion & icon stroke** — `duration: .2/.3` and `strokeWidth: 2/1.75` are JS numbers; `--duration-*`
499
+ and `--icon-stroke` reach consumer-emitted markup but not library internals.
500
+ - **Filled-control foreground** — `text-white` pinned on filled primary/destructive; the mapped
501
+ `--destructive-foreground` waits on the library reading it.
502
+
503
+ The stance: emit the complete token surface now so the library "just works" the moment it adopts each token;
504
+ do not build an override layer that fights the library and rots.
@@ -0,0 +1,138 @@
1
+ # Structure derivation
2
+
3
+ The second create phase. Runs after intake, before scaffold. Two steps, in order: the **object pass**, then **archetype assignment**. The output is one small, durable file — `docs/design/structure.md` — that every later phase reads and `scaffold.mjs` consumes.
4
+
5
+ The pass is never skipped. A small brief earns a proportionally small pass — a two-entity brief gets a two-entity map — but it always runs. Skipping straight to layout is the characteristic structural failure: screens that look plausible and model the domain wrongly. The structure phase gate requires a populated map before scaffold opens; an empty pass fails it.
6
+
7
+ ## The object pass
8
+
9
+ Extract the domain model from the brief. Record, per the map schema below:
10
+
11
+ - **Entities** — the nouns the operator works with (Ticket, Customer, Agent, Device). Extract what the brief and its domain support; do not fabricate entities the domain lacks, and do not promote an attribute to an entity.
12
+ - **Attributes** — the fields each entity carries.
13
+ - **Relationships and cardinalities** — how entities connect, recorded as `{to, cardinality}` (`one-to-one`, `one-to-many`, `many-to-one`, `many-to-many`).
14
+ - **Lifecycle states** — the ordered states an entity moves through (new → open → pending → resolved → closed). Omit for stateless entities.
15
+ - **Unit of work** — the one entity the operator's day turns on (the ticket, the case, the job). It anchors the home surface.
16
+ - **Core loop** — the verb sequence the operator runs on the unit of work (triage → assign → work → resolve → close).
17
+
18
+ Two modeling traps, both fatal to structure:
19
+
20
+ - **Metrics are not entities.** CAC, open-ticket-count, and p95-latency are instances of one Metric object, not entity rows. They render on a dashboard; they never earn their own list surface.
21
+ - **One entity, one home.** An entity's data and actions gather on one surface, cross-linked from everywhere else — never scattered across screens.
22
+
23
+ ## The temporal probe
24
+
25
+ Runs inside the object pass — temporal semantics are model facts, not styling facts, and the brief states none of them. Probe five questions:
26
+
27
+ - **Intermediate states** — does an action pass through pending / saving / processing / awaiting-approval before its terminal state?
28
+ - **Read-model lag** — is there delay between write and read visibility? Where yes, staleness is marked honestly downstream.
29
+ - **Deletion semantics** — archive, trash, hard delete, or regulatory retention? Each is a different surface and a different affordance.
30
+ - **History** — is prior state auditable or required? Its answer surfaces as a model fact — a History/audit entity, a timeline, or a versioned attribute.
31
+ - **Failure paths** — validation, server error, disconnection, concurrent edit.
32
+
33
+ Four answers land in the map's `temporal` block (`intermediate-states`, `read-model-lag`, `deletion`, `failure-paths`); history's answer lands wherever the model puts it (an entity, an attribute, a surface). The probe is what makes the lifecycle phase's states bindable — the tool spends most of its life in states the brief never mentions.
34
+
35
+ ## Archetype assignment
36
+
37
+ Each entity and workflow claims surfaces from the closed six-archetype catalog. `data/operator-pack/surface-catalog.md` is canonical for the choosing rules, the per-archetype density modifier, and the exemplar pointer; the assignment reads them. Orientation:
38
+
39
+ | archetype | claimed when |
40
+ | --- | --- |
41
+ | `list-detail` | a high-cardinality collection worked one item at a time — the unit of work's home |
42
+ | `dashboard` | a read-first overview answering a few standing questions across entities (Metric instances) |
43
+ | `entity-detail` | one rich entity's attributes, relationships, and actions gathered on a single surface |
44
+ | `create-edit` | minting or editing one entity — a stepper that mirrors edit; templates prefill |
45
+ | `log-stream` | append-only, high-volume, time-ordered events — read and filter, rarely edit |
46
+ | `command-center` | a live multi-pane monitor over several entities at once, with drill-down |
47
+
48
+ Membership is closed. New archetypes enter only as pack data, ledger-recorded — never invented per run.
49
+
50
+ **Navigation derives from the object model, never decorated on.** Top-level nav entries are the primary entities and their home surfaces; secondary entities are reached through their parent's detail surface, not promoted. The unit of work is the home. Cap top-level entries at what stays countable on one hand — nav categories the model does not carry are a structural defect, not a convenience.
51
+
52
+ ## The map: `docs/design/structure.md`
53
+
54
+ The derivation's whole output. Human prose above the block; the fenced `yaml structure` block is the machine contract — `scaffold.mjs` reads it, so the field names are law. Scaffold generates routes, navigation wiring, and per-surface state-shell stubs from `surfaces[]`, and keys fixture wiring off `entities[]`.
55
+
56
+ ```yaml structure
57
+ unit-of-work: <entity>
58
+ core-loop: <verb → verb → verb>
59
+ entities:
60
+ - name: <Entity>
61
+ attributes: [<field>, ...]
62
+ lifecycle: [<state>, ...] # omit for stateless entities
63
+ relationships:
64
+ - { to: <Entity>, cardinality: one-to-one | one-to-many | many-to-one | many-to-many }
65
+ temporal:
66
+ intermediate-states: [<state>, ...]
67
+ read-model-lag: <none | description>
68
+ deletion: <archive | trash | hard | retained>
69
+ failure-paths: [validation, server-error, disconnection, concurrent-edit, ...]
70
+ surfaces:
71
+ - name: <Surface>
72
+ archetype: list-detail | dashboard | entity-detail | create-edit | log-stream | command-center
73
+ route: /<path>
74
+ entity: <Entity>
75
+ density-modifier: <integer step; positive = denser; mirrors the catalog's per-archetype modifier>
76
+ states: [loading, empty, error, partial] # applicable subset; data surfaces carry all four
77
+ ```
78
+
79
+ ## OOUX structural diagnostics
80
+
81
+ Four judged diagnostics for a wrong object model (after Sophia Prater's OOUX vocabulary). Run them over the map and over any UI under review; each is a one-line detection question:
82
+
83
+ - **Shapeshifter** — is one entity rendered inconsistently across surfaces? (Does a Ticket's status pill look different in the queue than in the detail?)
84
+ - **Masked** — is an entity hidden inside another's UI, with no surface of its own? (Is Customer only ever a string on a Ticket, never its own object?)
85
+ - **Broken** — is an entity's data or actions split across surfaces with no complete home? (Are a Device's config and its telemetry on two unrelated screens?)
86
+ - **Isolated** — is an entity unreachable from the navigation and from its relationships? (Can the operator reach an Agent from anywhere?)
87
+
88
+ These are judgment, not machine rules — no waiver, no score. A finding is raised, ranked, and fixed at the model layer, never patched at the surface.
89
+
90
+ ## Worked example — a ticket queue
91
+
92
+ Brief: *"A support ticket queue. Agents triage incoming tickets, assign them, work them to resolution, and close them. Tickets come from customers."*
93
+
94
+ ```yaml structure
95
+ unit-of-work: Ticket
96
+ core-loop: triage → assign → work → resolve → close
97
+ entities:
98
+ - name: Ticket
99
+ attributes: [subject, body, status, priority, channel, requester, assignee, created_at, updated_at]
100
+ lifecycle: [new, open, pending, resolved, closed]
101
+ relationships:
102
+ - { to: Customer, cardinality: many-to-one }
103
+ - { to: Agent, cardinality: many-to-one }
104
+ - name: Customer
105
+ attributes: [name, email, org]
106
+ relationships:
107
+ - { to: Ticket, cardinality: one-to-many }
108
+ - name: Agent
109
+ attributes: [name, email, role, availability]
110
+ relationships:
111
+ - { to: Ticket, cardinality: one-to-many }
112
+ temporal:
113
+ intermediate-states: [assigning, saving-reply, pending-customer]
114
+ read-model-lag: none # single-writer per ticket
115
+ deletion: archive # tickets are retained for audit, never hard-deleted
116
+ failure-paths: [validation, server-error, disconnection, concurrent-edit]
117
+ surfaces:
118
+ - name: Ticket Queue
119
+ archetype: list-detail
120
+ route: /tickets
121
+ entity: Ticket
122
+ density-modifier: 1
123
+ states: [loading, empty, error, partial]
124
+ - name: Ticket Detail
125
+ archetype: entity-detail
126
+ route: /tickets/:id
127
+ entity: Ticket
128
+ density-modifier: 0
129
+ states: [loading, empty, error, partial]
130
+ - name: New Ticket
131
+ archetype: create-edit
132
+ route: /tickets/new
133
+ entity: Ticket
134
+ density-modifier: -1
135
+ states: [loading, error]
136
+ ```
137
+
138
+ Navigation: top-level is **Tickets** — the unit of work's home. Customer and Agent are reached through Ticket Detail, not promoted to top-level nav (derived, not decorated). History — the audit of status changes and replies — surfaces as a timeline on the detail, not a fifth temporal key. OOUX pass: the status pill renders identically on queue and detail (no Shapeshifter), and Customer links out to its own detail rather than living only as a string (no Masked).