@nurix/etna 0.19.2 → 0.19.4

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 (122) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/CLAUDE.md +2 -2
  3. package/data/harnesses/code/harness.json +2 -2
  4. package/data/harnesses/code/rules/frontend.md +3 -3
  5. package/data/harnesses/code/skills/design/SKILL.md +20 -8
  6. package/data/harnesses/code/skills/design/data/catalog/rules.csv +10 -5
  7. package/data/harnesses/code/skills/design/data/combinations/Readme.md +8 -4
  8. package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +113 -0
  9. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +11 -0
  10. package/data/harnesses/code/skills/design/data/combinations/manifest.json +57 -10
  11. package/data/harnesses/code/skills/design/data/combinations/motion.csv +10 -0
  12. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +92 -49
  13. package/data/harnesses/code/skills/design/data/combinations/styles.csv +67 -54
  14. package/data/harnesses/code/skills/design/data/combinations/typography.csv +64 -61
  15. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +4 -4
  16. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -5
  17. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +6 -6
  18. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -4
  19. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +4 -4
  20. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +2 -2
  21. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -6
  22. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +5 -5
  23. package/data/harnesses/code/skills/design/data/design-md/nurix.md +11 -7
  24. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +256 -141
  25. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +309 -1
  26. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +309 -1
  27. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +309 -1
  28. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +309 -1
  29. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +309 -1
  30. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +309 -1
  31. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +313 -1
  32. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +309 -1
  33. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +309 -1
  34. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +309 -1
  35. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +309 -1
  36. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +309 -1
  37. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +309 -1
  38. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +313 -1
  39. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +309 -1
  40. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +313 -1
  41. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +309 -1
  42. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +123 -123
  43. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +309 -1
  44. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +309 -1
  45. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +309 -1
  46. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +313 -1
  47. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +1 -1
  48. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +10 -0
  49. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +309 -1
  50. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +313 -1
  51. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +313 -1
  52. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +309 -1
  53. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +309 -1
  54. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +313 -1
  55. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +309 -1
  56. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +313 -1
  57. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +309 -1
  58. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +307 -2
  59. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +309 -1
  60. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +309 -1
  61. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +309 -1
  62. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +309 -1
  63. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +309 -1
  64. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +309 -1
  65. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +1 -1
  66. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +10 -0
  67. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +309 -1
  68. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +309 -1
  69. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +310 -2
  70. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +313 -1
  71. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +309 -1
  72. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +309 -1
  73. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +309 -1
  74. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +309 -1
  75. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +313 -1
  76. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +313 -1
  77. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +309 -1
  78. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +313 -1
  79. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +309 -1
  80. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +309 -1
  81. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +313 -1
  82. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +309 -1
  83. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +309 -1
  84. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +1 -1
  85. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +10 -0
  86. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +309 -1
  87. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +309 -1
  88. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +309 -1
  89. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +309 -1
  90. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +309 -1
  91. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +309 -1
  92. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +309 -1
  93. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +25 -25
  94. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +18 -18
  95. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +82 -105
  96. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +97 -93
  97. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +16 -15
  98. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +34 -34
  99. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +5 -5
  100. package/data/harnesses/code/skills/design/references/create.md +49 -7
  101. package/data/harnesses/code/skills/design/references/diagnosis.md +3 -3
  102. package/data/harnesses/code/skills/design/references/hard-rules.md +25 -3
  103. package/data/harnesses/code/skills/design/references/improve.md +32 -3
  104. package/data/harnesses/code/skills/design/references/interaction.md +3 -3
  105. package/data/harnesses/code/skills/design/references/introspect.md +1 -1
  106. package/data/harnesses/code/skills/design/references/lifecycle.md +2 -2
  107. package/data/harnesses/code/skills/design/references/personality.md +280 -133
  108. package/data/harnesses/code/skills/design/references/structure.md +22 -1
  109. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +306 -26
  110. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -104
  111. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +200 -11
  112. package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +65 -0
  113. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +95 -11
  114. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +50 -5
  115. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +214 -14
  116. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +242 -25
  117. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +81 -6
  118. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +101 -5
  119. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +142 -17
  120. package/data/harnesses/etna/CLAUDE.md +1 -1
  121. package/data/harnesses/etna/harness.json +2 -2
  122. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- // skills/design/scripts/validate-combinations.mjs — schema validator for the combination datasets (data/combinations/): file presence, byte-exact headers, sequential ids, tier/density/surface vocabularies, symbol-glyph ban, palette hex + WCAG contrast floors, chart token discipline, lucide/font registry membership, and manifest row-count parity. Importable (validateCombinations(dir)) and runnable as a CLI.
1
+ // skills/design/scripts/validate-combinations.mjs — schema validator for the combination datasets (data/combinations/): file presence, byte-exact headers, sequential ids, tier/density/surface/theme/budget vocabularies, symbol-glyph ban, palette hex + WCAG contrast floors, style payload + checklist discipline, typography @fontsource binding, motion token-name discipline, chart token discipline, lucide/font registry membership, accent-preset scale shape, and manifest row/tier-count parity. Importable (validateCombinations(dir)) and runnable as a CLI.
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { join, dirname, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -7,14 +7,15 @@ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
7
7
  const SKILL_ROOT = join(SCRIPTS_DIR, "..");
8
8
  const DEFAULT_DIR = join(SKILL_ROOT, "data/combinations");
9
9
 
10
- const CSV_FILES = ["typography.csv", "palettes.csv", "styles.csv", "product-router.csv", "charts.csv", "icons.csv", "landing.csv"];
11
- const REQUIRED_FILES = [...CSV_FILES, "manifest.json", "lucide-names.json", "fonts-known.json", "Readme.md"];
10
+ const CSV_FILES = ["typography.csv", "palettes.csv", "styles.csv", "motion.csv", "product-router.csv", "charts.csv", "icons.csv", "landing.csv"];
11
+ const REQUIRED_FILES = [...CSV_FILES, "manifest.json", "lucide-names.json", "fonts-known.json", "accent-presets.md", "Readme.md"];
12
12
 
13
13
  // Byte-exact, order-exact header contract per dataset.
14
14
  const HEADERS = {
15
- "typography.csv": ["id", "pairing", "tier", "heading_family", "body_family", "mono_family", "contrast", "mood", "best_for", "heading_weights", "body_weights", "google_fonts_css", "notes"],
16
- "palettes.csv": ["id", "palette", "tier", "product_affinity", "mood", "background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "input", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "warning", "chart-1", "chart-2", "chart-3", "chart-4", "chart-5", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring", "notes"],
17
- "styles.csv": ["id", "movement", "tier", "era", "keywords", "description", "palette_stance", "type_stance", "shape_stance", "motion_stance", "density_affinity", "best_for", "avoid_for", "token_leanings", "operator_note"],
15
+ "typography.csv": ["id", "pairing", "tier", "heading_family", "body_family", "mono_family", "contrast", "mood", "best_for", "heading_weights", "body_weights", "google_fonts_css", "notes", "css-import", "tailwind-config", "display-size-hint"],
16
+ "palettes.csv": ["id", "palette", "tier", "product_affinity", "theme", "mood", "background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "input", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "warning", "chart-1", "chart-2", "chart-3", "chart-4", "chart-5", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring", "notes"],
17
+ "styles.csv": ["id", "movement", "tier", "era", "keywords", "description", "palette_stance", "type_stance", "shape_stance", "motion_stance", "density_affinity", "best_for", "avoid_for", "token_leanings", "operator_note", "theme", "voice", "css-recipe", "checklist", "tokens-hint"],
18
+ "motion.csv": ["id", "moment", "tier", "keywords", "trigger", "duration_token", "easing_token", "transform_delta", "archetypes", "budget", "do", "dont", "performance_notes"],
18
19
  "product-router.csv": ["id", "product_type", "tier", "keywords", "style_priority", "palette_stance", "type_stance", "density_tier", "key_surfaces", "decision_rules", "anti_patterns", "notes"],
19
20
  "charts.csv": ["id", "data_shape", "tier", "keywords", "best_chart", "alternatives", "when", "when_not", "volume_guidance", "series_tokens", "a11y_notes", "fallback", "interaction"],
20
21
  "icons.csv": ["id", "intent", "tier", "keywords", "lucide_name", "import_name", "usage", "notes"],
@@ -24,7 +25,20 @@ const HEADERS = {
24
25
  const VALID_TIERS = new Set(["operator-active", "dormant-marketing"]);
25
26
  const VALID_DENSITIES = new Set(["spacious", "standard", "dense"]);
26
27
  const VALID_TYPE_CONTRASTS = new Set(["serif+sans", "sans+sans", "sans+serif", "serif+serif", "display+sans", "mono+sans"]);
27
- const VALID_KEY_SURFACES = new Set(["list-detail", "dashboard", "entity-detail", "create-edit", "log-stream", "command-center"]);
28
+ // The closed six surface archetypes — product-router key_surfaces and motion archetypes both draw from it.
29
+ const VALID_ARCHETYPES = new Set(["list-detail", "dashboard", "entity-detail", "create-edit", "log-stream", "command-center"]);
30
+ const VALID_STYLE_THEMES = new Set(["light", "dark", "both"]);
31
+ const VALID_PALETTE_THEMES = new Set(["light", "dark"]);
32
+ const VALID_BUDGETS = new Set(["baseline", "brand-forward"]);
33
+ // Legal temporal token names per design-tokens SCHEMA.md: the stored duration primitives (+ v2 stagger-unit) and ease-default plus its emit-derived ease-enter / ease-exit pair.
34
+ const MOTION_DURATION_TOKENS = new Set(["duration-unit", "duration-fast", "duration-normal", "duration-slow", "stagger-unit"]);
35
+ const MOTION_EASING_TOKENS = new Set(["ease-default", "ease-enter", "ease-exit"]);
36
+ // The four executable style payload columns — non-empty on every operator-active row.
37
+ const STYLE_PAYLOAD_COLUMNS = ["voice", "css-recipe", "checklist", "tokens-hint"];
38
+ // One @fontsource import statement, as the css-import cell carries them ;-separated.
39
+ const FONTSOURCE_IMPORT = /^@import '@fontsource(?:-variable)?\/([a-z0-9-]+)\/[a-z0-9.-]+\.css'$/;
40
+ const ACCENT_PRESET_SCALES = 12;
41
+ const ACCENT_PRESET_STEPS = 7;
28
42
 
29
43
  const UPPER_HEX = /^#[0-9A-F]{6}$/;
30
44
  const RAW_HEX_ANYWHERE = /#[0-9a-fA-F]{3,8}\b/;
@@ -141,9 +155,11 @@ function checkGeneric(file, csv, errors) {
141
155
  });
142
156
  }
143
157
 
144
- // palettes.csv: uppercase 6-digit hex in all 34 color cells + WCAG contrast floors.
158
+ // palettes.csv: theme enum, uppercase 6-digit hex in all 34 color cells + WCAG contrast floors.
145
159
  function checkPalettes(csv, errors) {
146
160
  for (const row of csv.rows) {
161
+ if (!VALID_PALETTE_THEMES.has(row.theme))
162
+ errors.push(`palettes.csv row ${row._line} (${row.palette}): invalid theme "${row.theme}" (want light|dark)`);
147
163
  const valid = {};
148
164
  for (const col of PALETTE_COLOR_COLUMNS) {
149
165
  const v = row[col];
@@ -163,6 +179,50 @@ function checkPalettes(csv, errors) {
163
179
  }
164
180
  }
165
181
 
182
+ // styles.csv: theme enum + the payload rule — all four payload cells non-empty on operator-active rows, the checklist carrying 3-7 ;-separated items.
183
+ function checkStyles(csv, errors) {
184
+ for (const row of csv.rows) {
185
+ if (!VALID_STYLE_THEMES.has(row.theme))
186
+ errors.push(`styles.csv row ${row._line} (${row.movement}): invalid theme "${row.theme}" (want light|dark|both)`);
187
+ if (row.tier !== "operator-active") continue;
188
+ for (const col of STYLE_PAYLOAD_COLUMNS)
189
+ if (row[col].trim() === "")
190
+ errors.push(`styles.csv row ${row._line} (${row.movement}): operator-active row has empty ${col} payload`);
191
+ const items = row.checklist.split(";").map((s) => s.trim()).filter((s) => s !== "");
192
+ if (row.checklist.trim() !== "" && (items.length < 3 || items.length > 7))
193
+ errors.push(`styles.csv row ${row._line} (${row.movement}): checklist has ${items.length} items (want 3-7)`);
194
+ }
195
+ }
196
+
197
+ // motion.csv: budget enum, archetypes from the closed six, duration/easing cells referencing only legal temporal token names.
198
+ function checkMotion(csv, errors) {
199
+ for (const row of csv.rows) {
200
+ if (!VALID_BUDGETS.has(row.budget))
201
+ errors.push(`motion.csv row ${row._line} (${row.moment}): invalid budget "${row.budget}" (want baseline|brand-forward)`);
202
+
203
+ const archetypes = row.archetypes.split(";").map((s) => s.trim()).filter((s) => s !== "");
204
+ if (archetypes.length === 0)
205
+ errors.push(`motion.csv row ${row._line} (${row.moment}): archetypes cell is empty`);
206
+ for (const a of archetypes)
207
+ if (!VALID_ARCHETYPES.has(a))
208
+ errors.push(`motion.csv row ${row._line} (${row.moment}): unknown archetype "${a}"`);
209
+
210
+ const durations = row.duration_token.match(/duration-[a-z]+|stagger-unit/g) ?? [];
211
+ if (durations.length === 0)
212
+ errors.push(`motion.csv row ${row._line} (${row.moment}): duration_token names no duration token`);
213
+ for (const tok of durations)
214
+ if (!MOTION_DURATION_TOKENS.has(tok))
215
+ errors.push(`motion.csv row ${row._line} (${row.moment}): duration token "${tok}" not in the schema vocabulary (${[...MOTION_DURATION_TOKENS].join(",")})`);
216
+
217
+ const easings = row.easing_token.match(/ease-[a-z]+/g) ?? [];
218
+ if (easings.length === 0)
219
+ errors.push(`motion.csv row ${row._line} (${row.moment}): easing_token names no easing token`);
220
+ for (const tok of easings)
221
+ if (!MOTION_EASING_TOKENS.has(tok))
222
+ errors.push(`motion.csv row ${row._line} (${row.moment}): easing token "${tok}" not in the schema vocabulary (${[...MOTION_EASING_TOKENS].join(",")})`);
223
+ }
224
+ }
225
+
166
226
  // charts.csv: operator-active only, --chart-N discipline in series_tokens, N in 1..5 everywhere, no raw hex.
167
227
  function checkCharts(csv, errors) {
168
228
  for (const row of csv.rows) {
@@ -199,7 +259,7 @@ function checkIcons(csv, dir, errors) {
199
259
  }
200
260
  }
201
261
 
202
- // typography.csv rows against the fonts-known.json registry (family -> known weights) + contrast vocabulary.
262
+ // typography.csv rows against the fonts-known.json registry (family -> known weights), the contrast vocabulary, and the paste-ready payload columns: css-import parses as @fontsource statements whose package set is exactly the row's kebab-cased families, tailwind-config names all three families, display-size-hint non-empty.
203
263
  function checkTypography(csv, dir, errors) {
204
264
  const text = readSafe(join(dir, "fonts-known.json"));
205
265
  if (text === null) return; // presence already reported
@@ -224,6 +284,31 @@ function checkTypography(csv, dir, errors) {
224
284
  }
225
285
  if (!VALID_TYPE_CONTRASTS.has(row.contrast))
226
286
  errors.push(`typography.csv row ${row._line} (${row.pairing}): invalid contrast "${row.contrast}"`);
287
+
288
+ // The font-binding contract at the data root: every import statement parses, and the imported package set is exactly the row's kebab-cased family set.
289
+ const families = [row.heading_family, row.body_family, row.mono_family];
290
+ const kebabs = new Set(families.map((f) => f.toLowerCase().replace(/ /g, "-")));
291
+ const pkgs = new Set();
292
+ const fragments = row["css-import"].split(";").map((s) => s.trim()).filter((s) => s !== "");
293
+ if (fragments.length === 0)
294
+ errors.push(`typography.csv row ${row._line} (${row.pairing}): css-import is empty`);
295
+ for (const frag of fragments) {
296
+ const m = FONTSOURCE_IMPORT.exec(frag);
297
+ if (!m) errors.push(`typography.csv row ${row._line} (${row.pairing}): css-import fragment "${frag}" is not an @import '@fontsource/<pkg>/....css' statement`);
298
+ else pkgs.add(m[1]);
299
+ }
300
+ for (const pkg of pkgs)
301
+ if (!kebabs.has(pkg))
302
+ errors.push(`typography.csv row ${row._line} (${row.pairing}): css-import package "${pkg}" matches none of the row's families`);
303
+ for (const kebab of kebabs)
304
+ if (fragments.length > 0 && !pkgs.has(kebab))
305
+ errors.push(`typography.csv row ${row._line} (${row.pairing}): family "${kebab}" has no css-import statement`);
306
+
307
+ for (const fam of new Set(families))
308
+ if (!row["tailwind-config"].includes(fam))
309
+ errors.push(`typography.csv row ${row._line} (${row.pairing}): tailwind-config does not name family "${fam}"`);
310
+ if (row["display-size-hint"].trim() === "")
311
+ errors.push(`typography.csv row ${row._line} (${row.pairing}): display-size-hint is empty`);
227
312
  }
228
313
  }
229
314
 
@@ -236,7 +321,7 @@ function checkDensityAndSurfaces(file, csv, errors) {
236
321
  errors.push(`${file} row ${row._line} (${row[nameCol]}): invalid ${densityCol} "${row[densityCol]}" (want spacious|standard|dense)`);
237
322
  if (file === "product-router.csv" && row.tier === "operator-active") {
238
323
  for (const surface of row.key_surfaces.split(",").map((s) => s.trim()).filter((s) => s !== ""))
239
- if (!VALID_KEY_SURFACES.has(surface))
324
+ if (!VALID_ARCHETYPES.has(surface))
240
325
  errors.push(`product-router.csv row ${row._line} (${row.product_type}): unknown key_surface "${surface}"`);
241
326
  }
242
327
  }
@@ -249,8 +334,32 @@ function checkLanding(csv, errors) {
249
334
  errors.push(`landing.csv row ${row._line} (${row.pattern}): tier must be dormant-marketing (got "${row.tier}")`);
250
335
  }
251
336
 
252
- // manifest.json: datasets entry per CSV with the required keys and a rows.total matching the actual row count.
253
- function checkManifest(dir, csvs, errors) {
337
+ // accent-presets.md: the accent table carries exactly 12 scales, each with 7 oklch step cells; returns the parsed scale count for manifest parity (null when unparseable).
338
+ function checkAccentPresets(dir, errors) {
339
+ const text = readSafe(join(dir, "accent-presets.md"));
340
+ if (text === null) return null; // presence already reported
341
+ const lines = text.split("\n");
342
+ const headerIdx = lines.findIndex((l) => /^\|\s*Accent\s*\|/.test(l));
343
+ if (headerIdx === -1) { errors.push("accent-presets.md: accent table header row (| Accent | ...) not found"); return null; }
344
+ const stepLabels = lines[headerIdx].split("|").map((c) => c.trim()).filter((c) => /^\d+$/.test(c));
345
+ if (stepLabels.length !== ACCENT_PRESET_STEPS)
346
+ errors.push(`accent-presets.md: accent table declares ${stepLabels.length} step columns (want ${ACCENT_PRESET_STEPS})`);
347
+ let scales = 0;
348
+ for (let i = headerIdx + 2; i < lines.length && lines[i].startsWith("|"); i++) {
349
+ const cells = lines[i].split("|").slice(1, -1).map((c) => c.trim());
350
+ const name = cells[0] ?? "";
351
+ scales++;
352
+ const steps = cells.filter((c) => /`oklch\([^)]*\)`/.test(c));
353
+ if (steps.length !== ACCENT_PRESET_STEPS)
354
+ errors.push(`accent-presets.md: scale ${name} carries ${steps.length} oklch steps (want ${ACCENT_PRESET_STEPS})`);
355
+ }
356
+ if (scales !== ACCENT_PRESET_SCALES)
357
+ errors.push(`accent-presets.md: accent table carries ${scales} scales (want ${ACCENT_PRESET_SCALES})`);
358
+ return scales;
359
+ }
360
+
361
+ // manifest.json: datasets entry per CSV with the required keys and rows.total + tier counts matching the actual CSV, plus an accent-presets.md entry whose scales match the parsed table.
362
+ function checkManifest(dir, csvs, accentScales, errors) {
254
363
  const text = readSafe(join(dir, "manifest.json"));
255
364
  if (text === null) return; // presence already reported
256
365
  let manifest;
@@ -261,11 +370,24 @@ function checkManifest(dir, csvs, errors) {
261
370
  if (entry === undefined) { errors.push(`manifest.json: datasets has no entry for ${file}`); continue; }
262
371
  for (const key of ["seed", "research", "quality_bar", "rows", "refresh_trigger"])
263
372
  if (!(key in entry)) errors.push(`manifest.json: datasets entry for ${file} missing key "${key}"`);
264
- const total = entry.rows && typeof entry.rows === "object" ? entry.rows.total : undefined;
265
- if (typeof total !== "number") errors.push(`manifest.json: datasets entry for ${file} has no numeric rows.total`);
266
- else if (csvs[file] && total !== csvs[file].rows.length)
267
- errors.push(`manifest.json: ${file} rows.total=${total} but the CSV has ${csvs[file].rows.length} data rows`);
373
+ if (!entry.rows || typeof entry.rows !== "object") { errors.push(`manifest.json: datasets entry for ${file} has no rows object`); continue; }
374
+ if (!csvs[file]) continue;
375
+ const actual = {
376
+ total: csvs[file].rows.length,
377
+ operator_active: csvs[file].rows.filter((r) => r.tier === "operator-active").length,
378
+ dormant_marketing: csvs[file].rows.filter((r) => r.tier === "dormant-marketing").length,
379
+ };
380
+ for (const key of ["total", "operator_active", "dormant_marketing"]) {
381
+ if (typeof entry.rows[key] !== "number") errors.push(`manifest.json: datasets entry for ${file} has no numeric rows.${key}`);
382
+ else if (entry.rows[key] !== actual[key])
383
+ errors.push(`manifest.json: ${file} rows.${key}=${entry.rows[key]} but the CSV has ${actual[key]}`);
384
+ }
268
385
  }
386
+ const accent = manifest.datasets["accent-presets.md"];
387
+ if (accent === undefined) errors.push("manifest.json: datasets has no entry for accent-presets.md");
388
+ else if (typeof accent.scales !== "number") errors.push("manifest.json: accent-presets.md entry has no numeric scales");
389
+ else if (accentScales !== null && accent.scales !== accentScales)
390
+ errors.push(`manifest.json: accent-presets.md scales=${accent.scales} but the table carries ${accentScales}`);
269
391
  }
270
392
 
271
393
  // Validate every combination dataset in `dir`; returns a self-integrity-shaped result.
@@ -286,13 +408,16 @@ export function validateCombinations(dir) {
286
408
  }
287
409
 
288
410
  if (csvs["palettes.csv"]) checkPalettes(csvs["palettes.csv"], errors);
411
+ if (csvs["styles.csv"]) checkStyles(csvs["styles.csv"], errors);
412
+ if (csvs["motion.csv"]) checkMotion(csvs["motion.csv"], errors);
289
413
  if (csvs["charts.csv"]) checkCharts(csvs["charts.csv"], errors);
290
414
  checkIcons(csvs["icons.csv"], dir, errors);
291
415
  checkTypography(csvs["typography.csv"], dir, errors);
292
416
  if (csvs["styles.csv"]) checkDensityAndSurfaces("styles.csv", csvs["styles.csv"], errors);
293
417
  if (csvs["product-router.csv"]) checkDensityAndSurfaces("product-router.csv", csvs["product-router.csv"], errors);
294
418
  if (csvs["landing.csv"]) checkLanding(csvs["landing.csv"], errors);
295
- checkManifest(dir, csvs, errors);
419
+ const accentScales = checkAccentPresets(dir, errors);
420
+ checkManifest(dir, csvs, accentScales, errors);
296
421
 
297
422
  const totalRows = CSV_FILES.reduce((n, f) => n + (csvs[f] ? csvs[f].rows.length : 0), 0);
298
423
  return { name, pass: errors.length === 0, detail: errors.length ? `${errors.length} issue(s)` : `${CSV_FILES.length} datasets, ${totalRows} rows validated`, errors };
@@ -148,4 +148,4 @@ For real code (the CLI under `packages/`, hook scripts):
148
148
 
149
149
  ## Runtime Notes
150
150
 
151
- etna runs in Claude Code. Sub-agent dispatch is the Agent tool; sub-agents can run skills and spawn their own sub-agents (probe-verified; re-verify on runtime updates), but cannot hold the browser connection (browser work is Claude in Chrome, main session only). The user-level memory store is `~/.claude/` (auto-memory under `~/.claude/projects/...`). The ownership index and skill/agent descriptions are injected at session start; skills are invoked as slash commands (e.g. `/brainstorm start`). If pinning dispatch tiers: strong tier (opus-class) for judgment-heavy work, fast tier (sonnet-class) for mechanical fan-out — tier names go stale, re-check on model releases.
151
+ etna runs in Claude Code. Sub-agent dispatch is the Agent tool; sub-agents can run skills and spawn their own sub-agents (probe-verified; re-verify on runtime updates), but cannot hold the browser connection (browser work is Claude in Chrome, main session only). The user-level memory store is `~/.claude/` (auto-memory under `~/.claude/projects/...`). The ownership index and skill/agent descriptions are injected at session start; skills are invoked as slash commands (e.g. `/brainstorm start`). Dispatch tiers: mechanical fan-out pins the fast tier (sonnet-class); a user-requested judgment-heavy dispatch takes the strong tier (opus-class) — tier names go stale, re-check on model releases.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.1.7",
4
+ "version": "1.1.8",
5
5
  "files": [
6
6
  ".prettierrc.json",
7
7
  ".prettierignore",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "142ff7b5b3ab"
10
+ "digest": "8313ddf886d3"
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.2",
3
+ "version": "0.19.4",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {