@nurix/etna 0.19.10 → 0.19.12

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 (491) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +1 -1
  3. package/data/harnesses/code/rules/frontend.md +5 -5
  4. package/data/harnesses/code/skills/design/SKILL.md +70 -50
  5. package/data/harnesses/code/skills/design/components/charts.md +76 -0
  6. package/data/harnesses/code/skills/design/components/collections.md +132 -0
  7. package/data/harnesses/code/skills/design/components/feedback.md +47 -0
  8. package/data/harnesses/code/skills/design/components/hooks.md +35 -0
  9. package/data/harnesses/code/skills/design/components/index.md +143 -0
  10. package/data/harnesses/code/skills/design/components/inputs.md +327 -0
  11. package/data/harnesses/code/skills/design/components/layout.md +52 -0
  12. package/data/harnesses/code/skills/design/components/media.md +66 -0
  13. package/data/harnesses/code/skills/design/components/motion.md +82 -0
  14. package/data/harnesses/code/skills/design/components/navigation.md +29 -0
  15. package/data/harnesses/code/skills/design/components/overlays.md +72 -0
  16. package/data/harnesses/code/skills/design/components/setup.md +40 -0
  17. package/data/harnesses/code/skills/design/components/typography.md +46 -0
  18. package/data/harnesses/code/skills/design/components/utils.md +37 -0
  19. package/data/harnesses/code/skills/design/data/blueprints/browse.md +38 -0
  20. package/data/harnesses/code/skills/design/data/blueprints/creation-flow.md +38 -0
  21. package/data/harnesses/code/skills/design/data/blueprints/dashboard.md +38 -0
  22. package/data/harnesses/code/skills/design/data/blueprints/grid.md +38 -0
  23. package/data/harnesses/code/skills/design/data/blueprints/queue.md +38 -0
  24. package/data/harnesses/code/skills/design/data/blueprints/record-detail.md +38 -0
  25. package/data/harnesses/code/skills/design/data/blueprints/settings.md +38 -0
  26. package/data/harnesses/code/skills/design/data/chart-styling/Readme.md +33 -0
  27. package/data/harnesses/code/skills/design/data/chart-styling/carbon-accessible-alert-semantic.md +19 -0
  28. package/data/harnesses/code/skills/design/data/chart-styling/carbon-monochrome-sequential-trend.md +19 -0
  29. package/data/harnesses/code/skills/design/data/chart-styling/datadog-classic-six-aligned.md +19 -0
  30. package/data/harnesses/code/skills/design/data/chart-styling/datawrapper-tiered-gray-hierarchy.md +19 -0
  31. package/data/harnesses/code/skills/design/data/chart-styling/econ-red-rule-horizontal.md +19 -0
  32. package/data/harnesses/code/skills/design/data/chart-styling/financial-diverging-threshold-overlay.md +19 -0
  33. package/data/harnesses/code/skills/design/data/chart-styling/ft-vocabulary-ranked-muted.md +19 -0
  34. package/data/harnesses/code/skills/design/data/chart-styling/grafana-dark-threshold-bands.md +19 -0
  35. package/data/harnesses/code/skills/design/data/chart-styling/material-rounded-explicit-label.md +19 -0
  36. package/data/harnesses/code/skills/design/data/chart-styling/nyt-ghost-grid-direct-label.md +19 -0
  37. package/data/harnesses/code/skills/design/data/chart-styling/plot-currentcolor-inherit.md +19 -0
  38. package/data/harnesses/code/skills/design/data/chart-styling/saas-gradient-glow-area.md +19 -0
  39. package/data/harnesses/code/skills/design/data/chart-styling/shadcn-composable-css-vars.md +19 -0
  40. package/data/harnesses/code/skills/design/data/charts/census.csv +58 -0
  41. package/data/harnesses/code/skills/design/data/checklists/browse.md +21 -0
  42. package/data/harnesses/code/skills/design/data/checklists/creation-flow.md +20 -0
  43. package/data/harnesses/code/skills/design/data/checklists/dashboard.md +21 -0
  44. package/data/harnesses/code/skills/design/data/checklists/grid.md +21 -0
  45. package/data/harnesses/code/skills/design/data/checklists/queue.md +21 -0
  46. package/data/harnesses/code/skills/design/data/checklists/record-detail.md +21 -0
  47. package/data/harnesses/code/skills/design/data/checklists/settings.md +21 -0
  48. package/data/harnesses/code/skills/design/data/chrome/board-column.md +68 -0
  49. package/data/harnesses/code/skills/design/data/chrome/data-table.md +63 -0
  50. package/data/harnesses/code/skills/design/data/chrome/side-panel.md +60 -0
  51. package/data/harnesses/code/skills/design/data/chrome/sidebar.md +78 -0
  52. package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +62 -0
  53. package/data/harnesses/code/skills/design/data/chrome/topbar.md +65 -0
  54. package/data/harnesses/code/skills/design/data/color/accent-presets.md +90 -0
  55. package/data/harnesses/code/skills/design/data/color/aesop-earth-mute.md +91 -0
  56. package/data/harnesses/code/skills/design/data/color/arc-peach-play.md +90 -0
  57. package/data/harnesses/code/skills/design/data/color/attio-hunter-teal.md +90 -0
  58. package/data/harnesses/code/skills/design/data/color/carbon-cyan-signal.md +90 -0
  59. package/data/harnesses/code/skills/design/data/color/clay-parchment.md +90 -0
  60. package/data/harnesses/code/skills/design/data/color/duolingo-feather.md +90 -0
  61. package/data/harnesses/code/skills/design/data/color/geist-mono-signal.md +90 -0
  62. package/data/harnesses/code/skills/design/data/color/linear-graphite-violet.md +90 -0
  63. package/data/harnesses/code/skills/design/data/color/mercury-onyx-cobalt.md +90 -0
  64. package/data/harnesses/code/skills/design/data/color/notion-paper-ink.md +90 -0
  65. package/data/harnesses/code/skills/design/data/color/radix-slate-iris.md +90 -0
  66. package/data/harnesses/code/skills/design/data/color/raycast-void-spectrum.md +90 -0
  67. package/data/harnesses/code/skills/design/data/color/spectrum-celery-fuchsia.md +90 -0
  68. package/data/harnesses/code/skills/design/data/color/stripe-blurple-ruby.md +90 -0
  69. package/data/harnesses/code/skills/design/data/color/superhuman-cod-picton.md +89 -0
  70. package/data/harnesses/code/skills/design/data/corpora/operations-review/companies.csv +15 -0
  71. package/data/harnesses/code/skills/design/data/corpora/operations-review/corpus.md +87 -0
  72. package/data/harnesses/code/skills/design/data/corpora/operations-review/people.csv +25 -0
  73. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/companies.csv +15 -0
  74. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/corpus.md +83 -0
  75. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/people.csv +25 -0
  76. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/companies.csv +15 -0
  77. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/corpus.md +89 -0
  78. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/people.csv +25 -0
  79. package/data/harnesses/code/skills/design/data/elevation/ant-design-directional-shadow.md +19 -0
  80. package/data/harnesses/code/skills/design/data/elevation/apple-liquid-glass-material.md +18 -0
  81. package/data/harnesses/code/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +20 -0
  82. package/data/harnesses/code/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +20 -0
  83. package/data/harnesses/code/skills/design/data/elevation/github-primer-sparing-shadow.md +19 -0
  84. package/data/harnesses/code/skills/design/data/elevation/ibm-carbon-layer-and-border.md +19 -0
  85. package/data/harnesses/code/skills/design/data/elevation/linear-dark-surface-ladder.md +17 -0
  86. package/data/harnesses/code/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +19 -0
  87. package/data/harnesses/code/skills/design/data/elevation/material-3-tonal-elevation.md +21 -0
  88. package/data/harnesses/code/skills/design/data/elevation/neobrutalist-hard-offset.md +19 -0
  89. package/data/harnesses/code/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +18 -0
  90. package/data/harnesses/code/skills/design/data/elevation/notion-warm-minimal-hairline.md +17 -0
  91. package/data/harnesses/code/skills/design/data/elevation/polaris-elevation-inset-bevel.md +21 -0
  92. package/data/harnesses/code/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +19 -0
  93. package/data/harnesses/code/skills/design/data/elevation/shadcn-neutral-derived-radius.md +19 -0
  94. package/data/harnesses/code/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +18 -0
  95. package/data/harnesses/code/skills/design/data/elevation/tailwind-default-utility-scale.md +19 -0
  96. package/data/harnesses/code/skills/design/data/elevation/vercel-geist-shadow-as-border.md +20 -0
  97. package/data/harnesses/code/skills/design/data/gradients/amber-attention-fill.md +16 -0
  98. package/data/harnesses/code/skills/design/data/gradients/cloud-shadow-wash.md +16 -0
  99. package/data/harnesses/code/skills/design/data/gradients/confident-lavender-wash.md +16 -0
  100. package/data/harnesses/code/skills/design/data/gradients/ember-ship-flare.md +18 -0
  101. package/data/harnesses/code/skills/design/data/gradients/full-metal-wash.md +16 -0
  102. package/data/harnesses/code/skills/design/data/gradients/indigo-ruby-cta-band.md +18 -0
  103. package/data/harnesses/code/skills/design/data/gradients/linear-graphite-wash.md +18 -0
  104. package/data/harnesses/code/skills/design/data/gradients/marble-quartz-wash.md +16 -0
  105. package/data/harnesses/code/skills/design/data/gradients/midnight-aurora-panel.md +16 -0
  106. package/data/harnesses/code/skills/design/data/gradients/neutral-volume-fill.md +16 -0
  107. package/data/harnesses/code/skills/design/data/gradients/revenue-uptrend-fill.md +16 -0
  108. package/data/harnesses/code/skills/design/data/gradients/risk-decline-fill.md +16 -0
  109. package/data/harnesses/code/skills/design/data/gradients/signal-blue-develop.md +18 -0
  110. package/data/harnesses/code/skills/design/data/gradients/slate-mist-wash.md +16 -0
  111. package/data/harnesses/code/skills/design/data/gradients/stripe-atmosphere-mesh.md +18 -0
  112. package/data/harnesses/code/skills/design/data/gradients/teal-signal-accent.md +16 -0
  113. package/data/harnesses/code/skills/design/data/gradients/violet-magenta-promo.md +16 -0
  114. package/data/harnesses/code/skills/design/data/iconography.md +199 -0
  115. package/data/harnesses/code/skills/design/data/motion/accordion-expand.md +18 -0
  116. package/data/harnesses/code/skills/design/data/motion/destructive-hold-confirm.md +18 -0
  117. package/data/harnesses/code/skills/design/data/motion/drag-dismiss-momentum.md +18 -0
  118. package/data/harnesses/code/skills/design/data/motion/empty-state-arrive.md +18 -0
  119. package/data/harnesses/code/skills/design/data/motion/hover-lift.md +18 -0
  120. package/data/harnesses/code/skills/design/data/motion/modal-center-arrive.md +18 -0
  121. package/data/harnesses/code/skills/design/data/motion/number-ticker-roll.md +18 -0
  122. package/data/harnesses/code/skills/design/data/motion/onboarding-bounce.md +18 -0
  123. package/data/harnesses/code/skills/design/data/motion/panel-slide.md +18 -0
  124. package/data/harnesses/code/skills/design/data/motion/press-feedback.md +18 -0
  125. package/data/harnesses/code/skills/design/data/motion/progress-linear-loop.md +18 -0
  126. package/data/harnesses/code/skills/design/data/motion/route-crossfade.md +18 -0
  127. package/data/harnesses/code/skills/design/data/motion/rubber-band-overscroll.md +18 -0
  128. package/data/harnesses/code/skills/design/data/motion/skeleton-shimmer.md +18 -0
  129. package/data/harnesses/code/skills/design/data/motion/stagger-arrive.md +18 -0
  130. package/data/harnesses/code/skills/design/data/motion/toast-enter-stack.md +18 -0
  131. package/data/harnesses/code/skills/design/data/motion/tooltip-chain-instant.md +18 -0
  132. package/data/harnesses/code/skills/design/data/motion/trigger-anchored-reveal.md +18 -0
  133. package/data/harnesses/code/skills/design/data/tells.csv +34 -0
  134. package/data/harnesses/code/skills/design/data/typography/anton-work-sans.md +17 -0
  135. package/data/harnesses/code/skills/design/data/typography/archivo-black-archivo.md +17 -0
  136. package/data/harnesses/code/skills/design/data/typography/bitter-karla.md +17 -0
  137. package/data/harnesses/code/skills/design/data/typography/dm-serif-dm-sans.md +17 -0
  138. package/data/harnesses/code/skills/design/data/typography/fraunces-inter.md +17 -0
  139. package/data/harnesses/code/skills/design/data/typography/geist-geist-mono.md +17 -0
  140. package/data/harnesses/code/skills/design/data/typography/hubot-sans-mona-sans.md +17 -0
  141. package/data/harnesses/code/skills/design/data/typography/ibm-plex-sans-operator-single-family.md +17 -0
  142. package/data/harnesses/code/skills/design/data/typography/instrument-serif-instrument-sans.md +17 -0
  143. package/data/harnesses/code/skills/design/data/typography/inter-operator-single-family.md +17 -0
  144. package/data/harnesses/code/skills/design/data/typography/libre-baskerville-libre-franklin.md +17 -0
  145. package/data/harnesses/code/skills/design/data/typography/newsreader-inter.md +17 -0
  146. package/data/harnesses/code/skills/design/data/typography/playfair-display-source-sans.md +17 -0
  147. package/data/harnesses/code/skills/design/data/typography/public-sans-operator-single-family.md +17 -0
  148. package/data/harnesses/code/skills/design/data/typography/sohne-inter-fintech.md +17 -0
  149. package/data/harnesses/code/skills/design/data/typography/source-serif-source-sans.md +17 -0
  150. package/data/harnesses/code/skills/design/data/typography/space-grotesk-work-sans-space-mono.md +17 -0
  151. package/data/harnesses/code/skills/design/data/typography/spectral-manrope.md +17 -0
  152. package/data/harnesses/code/skills/design/data/voices/bright-collaborator.md +29 -0
  153. package/data/harnesses/code/skills/design/data/voices/direct-operator.md +29 -0
  154. package/data/harnesses/code/skills/design/data/voices/earnest-helper.md +29 -0
  155. package/data/harnesses/code/skills/design/data/voices/editorial-composed.md +29 -0
  156. package/data/harnesses/code/skills/design/data/voices/quiet-concierge.md +29 -0
  157. package/data/harnesses/code/skills/design/data/voices/warm-guide.md +29 -0
  158. package/data/harnesses/code/skills/design/design-md/SCHEMA.md +179 -0
  159. package/data/harnesses/code/skills/design/design-md/index.csv +67 -0
  160. package/data/harnesses/code/skills/design/design-md/library/airbnb.md +207 -0
  161. package/data/harnesses/code/skills/design/design-md/library/airtable.md +205 -0
  162. package/data/harnesses/code/skills/design/design-md/library/anthropic-claude.md +203 -0
  163. package/data/harnesses/code/skills/design/design-md/library/apple.md +206 -0
  164. package/data/harnesses/code/skills/design/design-md/library/binance.md +210 -0
  165. package/data/harnesses/code/skills/design/design-md/library/bmw.md +210 -0
  166. package/data/harnesses/code/skills/design/design-md/library/cal.md +209 -0
  167. package/data/harnesses/code/skills/design/design-md/library/clay.md +204 -0
  168. package/data/harnesses/code/skills/design/design-md/library/clickhouse.md +205 -0
  169. package/data/harnesses/code/skills/design/design-md/library/cohere.md +205 -0
  170. package/data/harnesses/code/skills/design/design-md/library/coinbase.md +198 -0
  171. package/data/harnesses/code/skills/design/design-md/library/composio.md +198 -0
  172. package/data/harnesses/code/skills/design/design-md/library/cursor.md +197 -0
  173. package/data/harnesses/code/skills/design/design-md/library/elevenlabs.md +199 -0
  174. package/data/harnesses/code/skills/design/design-md/library/expo.md +200 -0
  175. package/data/harnesses/code/skills/design/design-md/library/figma.md +199 -0
  176. package/data/harnesses/code/skills/design/design-md/library/framer.md +202 -0
  177. package/data/harnesses/code/skills/design/design-md/library/hashicorp.md +205 -0
  178. package/data/harnesses/code/skills/design/design-md/library/ibm.md +205 -0
  179. package/data/harnesses/code/skills/design/design-md/library/intercom.md +204 -0
  180. package/data/harnesses/code/skills/design/design-md/library/kraken.md +200 -0
  181. package/data/harnesses/code/skills/design/design-md/library/linear-app.md +198 -0
  182. package/data/harnesses/code/skills/design/design-md/library/lovable.md +199 -0
  183. package/data/harnesses/code/skills/design/design-md/library/meta.md +204 -0
  184. package/data/harnesses/code/skills/design/design-md/library/minimax.md +204 -0
  185. package/data/harnesses/code/skills/design/design-md/library/mintlify.md +210 -0
  186. package/data/harnesses/code/skills/design/design-md/library/miro.md +209 -0
  187. package/data/harnesses/code/skills/design/design-md/library/mistral-ai.md +201 -0
  188. package/data/harnesses/code/skills/design/design-md/library/mongodb.md +201 -0
  189. package/data/harnesses/code/skills/design/design-md/library/notion.md +204 -0
  190. package/data/harnesses/code/skills/design/design-md/library/nvidia.md +215 -0
  191. package/data/harnesses/code/skills/design/design-md/library/ollama.md +213 -0
  192. package/data/harnesses/code/skills/design/design-md/library/opencode-ai.md +216 -0
  193. package/data/harnesses/code/skills/design/design-md/library/posthog.md +218 -0
  194. package/data/harnesses/code/skills/design/design-md/library/raycast.md +214 -0
  195. package/data/harnesses/code/skills/design/design-md/library/replicate.md +202 -0
  196. package/data/harnesses/code/skills/design/design-md/library/resend.md +203 -0
  197. package/data/harnesses/code/skills/design/design-md/library/revolut.md +203 -0
  198. package/data/harnesses/code/skills/design/design-md/library/runwayml.md +203 -0
  199. package/data/harnesses/code/skills/design/design-md/library/sanity.md +200 -0
  200. package/data/harnesses/code/skills/design/design-md/library/sentry.md +201 -0
  201. package/data/harnesses/code/skills/design/design-md/library/shopify.md +202 -0
  202. package/data/harnesses/code/skills/design/design-md/library/slack.md +202 -0
  203. package/data/harnesses/code/skills/design/design-md/library/spacex.md +206 -0
  204. package/data/harnesses/code/skills/design/design-md/library/spotify.md +206 -0
  205. package/data/harnesses/code/skills/design/design-md/library/stripe.md +196 -0
  206. package/data/harnesses/code/skills/design/design-md/library/supabase.md +196 -0
  207. package/data/harnesses/code/skills/design/design-md/library/superhuman.md +194 -0
  208. package/data/harnesses/code/skills/design/design-md/library/tesla.md +198 -0
  209. package/data/harnesses/code/skills/design/design-md/library/theverge.md +199 -0
  210. package/data/harnesses/code/skills/design/design-md/library/together-ai.md +193 -0
  211. package/data/harnesses/code/skills/design/design-md/library/uber.md +195 -0
  212. package/data/harnesses/code/skills/design/design-md/library/vercel.md +195 -0
  213. package/data/harnesses/code/skills/design/design-md/library/voltagent.md +197 -0
  214. package/data/harnesses/code/skills/design/design-md/library/warp.md +196 -0
  215. package/data/harnesses/code/skills/design/design-md/library/webflow.md +197 -0
  216. package/data/harnesses/code/skills/design/design-md/library/wired.md +197 -0
  217. package/data/harnesses/code/skills/design/design-md/library/wise.md +198 -0
  218. package/data/harnesses/code/skills/design/design-md/library/x-ai.md +199 -0
  219. package/data/harnesses/code/skills/design/design-md/library/zapier.md +198 -0
  220. package/data/harnesses/code/skills/design/design-md/nurix.md +214 -0
  221. package/data/harnesses/code/skills/design/design-md/variants/editorial-luxury.md +184 -0
  222. package/data/harnesses/code/skills/design/design-md/variants/expressive-saas.md +216 -0
  223. package/data/harnesses/code/skills/design/design-md/variants/quiet-premium.md +211 -0
  224. package/data/harnesses/code/skills/design/design-md/variants/terse-dense-operator.md +212 -0
  225. package/data/harnesses/code/skills/design/design-md/variants/warm-approachable.md +216 -0
  226. package/data/harnesses/code/skills/design/references/compose.md +23 -0
  227. package/data/harnesses/code/skills/design/references/content.md +28 -0
  228. package/data/harnesses/code/skills/design/references/emission.md +21 -0
  229. package/data/harnesses/code/skills/design/references/existing-apps.md +27 -0
  230. package/data/harnesses/code/skills/design/references/intake.md +36 -0
  231. package/data/harnesses/code/skills/design/references/object-pass.md +24 -0
  232. package/data/harnesses/code/skills/design/references/ship-gate.md +29 -0
  233. package/data/harnesses/code/skills/design/references/ux-patterns/Readme.md +195 -0
  234. package/data/harnesses/code/skills/design/references/ux-patterns/ai-layer.md +49 -0
  235. package/data/harnesses/code/skills/design/references/ux-patterns/attributes-lifecycle.md +43 -0
  236. package/data/harnesses/code/skills/design/references/ux-patterns/container-choice.md +44 -0
  237. package/data/harnesses/code/skills/design/references/ux-patterns/creation-paths.md +46 -0
  238. package/data/harnesses/code/skills/design/references/ux-patterns/dashboards.md +42 -0
  239. package/data/harnesses/code/skills/design/references/ux-patterns/modes.md +42 -0
  240. package/data/harnesses/code/skills/design/references/ux-patterns/object-model.md +46 -0
  241. package/data/harnesses/code/skills/design/references/ux-patterns/page-archetype.md +48 -0
  242. package/data/harnesses/code/skills/design/references/ux-patterns/promotion-demotion.md +37 -0
  243. package/data/harnesses/code/skills/design/references/ux-patterns/settings.md +42 -0
  244. package/data/harnesses/code/skills/design/references/ux-patterns/standard-pages.md +46 -0
  245. package/data/harnesses/code/skills/design/references/ux-patterns/state-matrix.md +41 -0
  246. package/data/harnesses/code/skills/design/references/ux-patterns/table-mechanics.md +48 -0
  247. package/data/harnesses/code/skills/design/references/ux-patterns/verbs.md +45 -0
  248. package/data/harnesses/code/skills/design/references/ux-patterns/viewer-context.md +40 -0
  249. package/data/harnesses/code/skills/design/scripts/README-validator.md +27 -0
  250. package/data/harnesses/code/skills/design/scripts/select-chart.mjs +596 -0
  251. package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +1198 -0
  252. package/data/harnesses/discussion/CLAUDE.md +1 -1
  253. package/data/harnesses/discussion/agents/discussion-baseline.md +27 -2
  254. package/data/harnesses/discussion/harness.json +2 -2
  255. package/data/harnesses/discussion/rules/artifacts.md +2 -0
  256. package/data/harnesses/discussion/rules/method.md +5 -1
  257. package/package.json +1 -1
  258. package/data/harnesses/code/skills/design/data/catalog/rules.csv +0 -137
  259. package/data/harnesses/code/skills/design/data/combinations/Readme.md +0 -33
  260. package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +0 -113
  261. package/data/harnesses/code/skills/design/data/combinations/charts.csv +0 -33
  262. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +0 -448
  263. package/data/harnesses/code/skills/design/data/combinations/icons.csv +0 -157
  264. package/data/harnesses/code/skills/design/data/combinations/landing.csv +0 -38
  265. package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +0 -2001
  266. package/data/harnesses/code/skills/design/data/combinations/manifest.json +0 -330
  267. package/data/harnesses/code/skills/design/data/combinations/motion.csv +0 -10
  268. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +0 -92
  269. package/data/harnesses/code/skills/design/data/combinations/product-router.csv +0 -80
  270. package/data/harnesses/code/skills/design/data/combinations/styles.csv +0 -67
  271. package/data/harnesses/code/skills/design/data/combinations/typography.csv +0 -64
  272. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +0 -106
  273. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +0 -154
  274. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +0 -151
  275. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +0 -139
  276. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +0 -139
  277. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +0 -140
  278. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +0 -140
  279. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +0 -140
  280. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +0 -139
  281. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +0 -139
  282. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +0 -147
  283. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +0 -156
  284. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +0 -138
  285. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +0 -138
  286. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +0 -137
  287. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -148
  288. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -141
  289. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +0 -156
  290. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +0 -140
  291. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +0 -153
  292. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +0 -158
  293. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +0 -156
  294. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -122
  295. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +0 -140
  296. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +0 -139
  297. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +0 -139
  298. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +0 -139
  299. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +0 -153
  300. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +0 -140
  301. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +0 -155
  302. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +0 -140
  303. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +0 -141
  304. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +0 -145
  305. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +0 -142
  306. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +0 -152
  307. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +0 -137
  308. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +0 -140
  309. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +0 -170
  310. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +0 -145
  311. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -137
  312. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +0 -168
  313. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +0 -163
  314. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +0 -147
  315. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +0 -158
  316. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +0 -171
  317. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +0 -166
  318. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +0 -160
  319. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +0 -173
  320. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +0 -145
  321. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +0 -153
  322. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +0 -167
  323. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +0 -140
  324. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +0 -140
  325. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +0 -140
  326. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +0 -130
  327. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +0 -140
  328. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +0 -140
  329. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +0 -139
  330. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +0 -137
  331. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +0 -138
  332. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +0 -158
  333. package/data/harnesses/code/skills/design/data/design-md/nurix.md +0 -138
  334. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +0 -695
  335. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +0 -20
  336. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +0 -1272
  337. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +0 -20
  338. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +0 -1268
  339. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +0 -20
  340. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +0 -1273
  341. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +0 -20
  342. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +0 -1262
  343. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +0 -20
  344. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +0 -1257
  345. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +0 -20
  346. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +0 -1270
  347. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +0 -20
  348. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +0 -1270
  349. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +0 -20
  350. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +0 -1261
  351. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +0 -20
  352. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +0 -1268
  353. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +0 -20
  354. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +0 -1281
  355. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +0 -20
  356. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +0 -1262
  357. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +0 -20
  358. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +0 -1271
  359. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +0 -20
  360. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +0 -1276
  361. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +0 -20
  362. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +0 -1293
  363. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +0 -20
  364. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +0 -1270
  365. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +0 -20
  366. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +0 -1276
  367. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +0 -20
  368. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +0 -1267
  369. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +0 -251
  370. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +0 -20
  371. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +0 -1273
  372. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +0 -20
  373. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +0 -1286
  374. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +0 -20
  375. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +0 -1290
  376. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +0 -20
  377. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +0 -1270
  378. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +0 -23
  379. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +0 -599
  380. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +0 -1272
  381. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +0 -20
  382. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +0 -1267
  383. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +0 -20
  384. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +0 -1289
  385. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +0 -20
  386. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +0 -1258
  387. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +0 -20
  388. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +0 -1284
  389. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +0 -20
  390. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +0 -1291
  391. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +0 -20
  392. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +0 -1263
  393. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +0 -20
  394. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +0 -1270
  395. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +0 -20
  396. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +0 -1261
  397. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +0 -20
  398. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +0 -1209
  399. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +0 -20
  400. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +0 -1265
  401. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +0 -20
  402. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +0 -1253
  403. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +0 -20
  404. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +0 -1268
  405. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +0 -20
  406. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +0 -1261
  407. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +0 -20
  408. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +0 -1263
  409. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +0 -20
  410. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +0 -1266
  411. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +0 -23
  412. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +0 -623
  413. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +0 -1276
  414. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +0 -20
  415. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +0 -1264
  416. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +0 -20
  417. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +0 -1268
  418. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +0 -20
  419. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +0 -1270
  420. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +0 -20
  421. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +0 -1288
  422. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +0 -20
  423. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +0 -1272
  424. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +0 -20
  425. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +0 -1265
  426. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +0 -20
  427. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +0 -1287
  428. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +0 -20
  429. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +0 -1286
  430. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +0 -20
  431. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +0 -1285
  432. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +0 -20
  433. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +0 -1287
  434. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +0 -20
  435. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +0 -1278
  436. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +0 -20
  437. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +0 -1269
  438. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +0 -20
  439. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +0 -1285
  440. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +0 -20
  441. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +0 -1303
  442. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +0 -20
  443. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +0 -1274
  444. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +0 -20
  445. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +0 -1296
  446. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +0 -23
  447. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +0 -612
  448. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +0 -1282
  449. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +0 -20
  450. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +0 -1287
  451. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +0 -20
  452. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +0 -1268
  453. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +0 -20
  454. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +0 -1276
  455. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +0 -20
  456. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +0 -1273
  457. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +0 -20
  458. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +0 -1296
  459. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +0 -20
  460. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +0 -1286
  461. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +0 -249
  462. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +0 -249
  463. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +0 -225
  464. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +0 -249
  465. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +0 -224
  466. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +0 -249
  467. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +0 -88
  468. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +0 -132
  469. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +0 -76
  470. package/data/harnesses/code/skills/design/references/create.md +0 -173
  471. package/data/harnesses/code/skills/design/references/diagnosis.md +0 -108
  472. package/data/harnesses/code/skills/design/references/hard-rules.md +0 -177
  473. package/data/harnesses/code/skills/design/references/improve.md +0 -116
  474. package/data/harnesses/code/skills/design/references/interaction.md +0 -77
  475. package/data/harnesses/code/skills/design/references/introspect.md +0 -100
  476. package/data/harnesses/code/skills/design/references/lifecycle.md +0 -73
  477. package/data/harnesses/code/skills/design/references/personality.md +0 -575
  478. package/data/harnesses/code/skills/design/references/structure.md +0 -159
  479. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +0 -1312
  480. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +0 -1498
  481. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +0 -551
  482. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +0 -1092
  483. package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +0 -26
  484. package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +0 -65
  485. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +0 -538
  486. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +0 -312
  487. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +0 -890
  488. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +0 -769
  489. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +0 -675
  490. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +0 -164
  491. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +0 -439
@@ -1,1498 +0,0 @@
1
- #!/usr/bin/env node
2
- // skills/design/scripts/derive-personality.mjs — design-tokens artifact resolver, coverage gate (--check; SCHEMA.md rules 1–15), and CSS emitter (--emit) for the operator-UI personality system (D29, D6).
3
- //
4
- // The reference consumer of data/design-tokens/SCHEMA.md (schema design-tokens/v2): resolves an
5
- // artifact directory (design.tokens.json + design.resolver.json [+ design.tokens.dark.json]) through
6
- // the resolver's deterministic order — concatenate the set files, overlay the selected theme context,
7
- // resolve aliases — then validates (rules 1–15) or emits personality.css / globals.css / fonts.css /
8
- // the <html> attributes. --emit refuses while any token carries a hint; --provisional (golden/
9
- // regression flows only, never shipped output) emits hinted tokens at their provisional $values
10
- // instead. --emit also enforces two emission contracts: font binding (contract 2, cod-16 — every
11
- // emitted stack family binds to a generated @fontsource import / installed package or a system
12
- // generic, fail-closed) and emitter completeness (contract 3 — an authored token group with no
13
- // emitter refuses to emit, naming the group).
14
- //
15
- // Usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir> [--provisional]] [--theme light|dark]
16
-
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
- import { dirname, join } from "node:path";
19
- import { fileURLToPath } from "node:url";
20
- import { SYSTEM_FONT_KEYWORDS, familySlug } from "./lib/checks.mjs";
21
-
22
- const EXT = "com.nurix.design";
23
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
24
- const COMBINATIONS_DIR = join(SCRIPT_DIR, "..", "data", "combinations");
25
-
26
- // The 27 visual primitives and their DTCG $type, in emission order (leaf names are law; no leading --).
27
- const PRIMITIVE_TYPES = new Map([
28
- ["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
29
- ["density-gap", "number"], ["density-inset", "number"],
30
- ["font-sans", "fontFamily"], ["font-mono", "fontFamily"],
31
- ["font-weight-normal", "fontWeight"], ["font-weight-strong", "fontWeight"],
32
- ["heading-tracking", "number"],
33
- ["duration-unit", "duration"], ["duration-fast", "duration"], ["duration-normal", "duration"],
34
- ["duration-slow", "duration"], ["ease-default", "cubicBezier"], ["stagger-unit", "duration"],
35
- ["contrast-boost", "number"],
36
- ["icon-stroke", "number"], ["icon-size-base", "dimension"],
37
- ["sidebar-width-expanded", "dimension"], ["sidebar-width-collapsed", "dimension"],
38
- ["topbar-height", "dimension"], ["side-panel-width", "dimension"],
39
- ["side-panel-width-rich", "dimension"], ["modal-width-max", "dimension"],
40
- ["form-max-width", "dimension"], ["table-row-height", "dimension"],
41
- ]);
42
- const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
43
-
44
- // The 19 semantic color tokens, in emission order.
45
- const SEMANTIC_TOKENS = [
46
- "background", "foreground", "card", "card-foreground", "muted", "muted-foreground",
47
- "border", "input", "ring", "primary", "primary-foreground", "accent", "accent-foreground",
48
- "destructive", "destructive-foreground", "success", "warning",
49
- "surface-raised", "surface-overlay",
50
- ];
51
-
52
- // The 17 substrate color tokens (the D6 set), in emission order.
53
- const SUBSTRATE_TOKENS = [
54
- "popover", "popover-foreground", "secondary", "secondary-foreground",
55
- "chart-1", "chart-2", "chart-3", "chart-4", "chart-5",
56
- "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground",
57
- "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring",
58
- ];
59
-
60
- const ALL_TOKENS = [...SEMANTIC_TOKENS, ...SUBSTRATE_TOKENS];
61
-
62
- const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption", "display"];
63
- const ELEVATION_TOKENS = ["sm", "md", "lg", "overlay"];
64
- const WASH_TOKENS = ["surface", "emphasis"];
65
- const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
66
- const LEGAL_FEEDBACK = ["subtle", "minimal"];
67
- const LEGAL_DENSITY = ["spacious", "standard", "dense"];
68
- const LEGAL_PROVENANCE = ["project", "extracted", "library", "default"];
69
- const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography", "shadow", "gradient"];
70
- const LEGAL_THEMES = ["light", "dark"];
71
- const MANIFEST_KEYS = ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"];
72
-
73
- // The optional persisted intake look block's enums — mirrored from phase-gate.mjs validateLookBlock
74
- // (the intake authority) so a persisted payload.look cannot rot unchecked.
75
- const LOOK_THEMES = ["light", "dark", "both"];
76
- const LOOK_DELIGHTS = ["baseline", "expressive"];
77
- const LOOK_RESOLUTION_MODES = ["confirmed", "recorded"];
78
- const LOOK_RESOLUTION_RE = /^(project|default|library:.+|seed:.+\+.+)$/i;
79
-
80
- // Contract 3 — the emitter registry: every authored token group's named consumer in the emitted
81
- // output. --emit refuses an authored group absent from this map (a group with no emitter vanishes).
82
- const EMITTED_GROUPS = new Map([
83
- ["primitive", "the [data-style] primitives block"],
84
- ["color", "the armored theme blocks (:root, :root[data-theme=\"light\"] / :root[data-theme=\"dark\"])"],
85
- ["typography", "the @theme ramp text utilities (text-h1 … text-display)"],
86
- ["elevation", "the --shadow-sm/md/lg/overlay composites + @theme shadow utilities"],
87
- ["wash", "--wash-surface / --wash-emphasis (optional presence)"],
88
- ]);
89
- const TYPOGRAPHY_SUBGROUPS = ["family", "ramp"];
90
-
91
- // Legal $description opening tags (rule 15): the provenance-layer vocabulary (source/brief/deviation,
92
- // 00-layering.md Composition A) plus the established mechanical-annotation prefixes.
93
- const LEGAL_DESCRIPTION_TAGS = ["source", "brief", "deviation", "derived", "corpus", "rationale", "note", "synthesized", "brand", "dark"];
94
-
95
- // Shadow-level names ranked by increasing intensity, for the ordering check.
96
- const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7, overlay: 8 };
97
-
98
- // ---------------------------------------------------------------------------
99
- // CSS value parsers — the parse-back authority (rule 9); byte-compatible with convert-design-md.mjs
100
- // ---------------------------------------------------------------------------
101
-
102
- // Round to at most 5 decimals (the srgb component precision).
103
- function round5(n) {
104
- return Math.round(n * 1e5) / 1e5;
105
- }
106
-
107
- // Strip one layer of matching surrounding quotes and unescape \" within double quotes.
108
- function unquote(raw) {
109
- const s = raw.trim();
110
- if (s.length >= 2 && s[0] === '"' && s.at(-1) === '"') return s.slice(1, -1).replaceAll('\\"', '"');
111
- if (s.length >= 2 && s[0] === "'" && s.at(-1) === "'") return s.slice(1, -1);
112
- return s;
113
- }
114
-
115
- // Split on a delimiter at bracket-depth 0 and outside quotes.
116
- function splitTopLevel(str, delim) {
117
- const parts = [];
118
- let depth = 0, inDouble = false, inSingle = false, cur = "";
119
- for (let i = 0; i < str.length; i++) {
120
- const c = str[i];
121
- if (c === '"' && !inSingle && str[i - 1] !== "\\") { inDouble = !inDouble; cur += c; }
122
- else if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; }
123
- else if (!inDouble && !inSingle && (c === "[" || c === "{" || c === "(")) { depth++; cur += c; }
124
- else if (!inDouble && !inSingle && (c === "]" || c === "}" || c === ")")) { depth--; cur += c; }
125
- else if (!inDouble && !inSingle && depth === 0 && c === delim) { parts.push(cur); cur = ""; }
126
- else cur += c;
127
- }
128
- parts.push(cur);
129
- return parts;
130
- }
131
-
132
- // Parse any corpus color string (oklch / #rrggbb / #rrggbbaa / rgb() / rgba()) into a typed DTCG color $value.
133
- function parseColorCss(css) {
134
- const s = css.trim();
135
- let m = /^oklch\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s);
136
- if (m) {
137
- const value = { colorSpace: "oklch", components: [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])] };
138
- if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
139
- return value;
140
- }
141
- m = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
142
- if (m) {
143
- const bytes = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
144
- const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex: `#${m[1].toLowerCase()}` };
145
- if (m[2] !== undefined) value.alpha = round5(parseInt(m[2], 16) / 255);
146
- return value;
147
- }
148
- m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
149
- if (!m) m = /^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s); // modern space syntax
150
- if (m) {
151
- const bytes = [m[1], m[2], m[3]].map(Number);
152
- const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
153
- const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
154
- if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
155
- return value;
156
- }
157
- throw new Error(`unparseable color: ${JSON.stringify(css)}`);
158
- }
159
-
160
- // Parse one shadow-list string (the elevation byte form) into the DTCG layer array. A layer color's
161
- // alpha may ride the master multiplier as calc(A * var(--shadow-intensity)) — A is the layer alpha.
162
- function parseShadowCss(css) {
163
- return splitTopLevel(css, ",").map((layerRaw) => {
164
- const s = layerRaw.trim();
165
- const colorStart = s.search(/rgba?\(|oklch\(|hsla?\(|#[0-9a-fA-F]/);
166
- if (colorStart < 0) throw new Error(`shadow layer names no color: ${JSON.stringify(s)}`);
167
- const colorCss = s.slice(colorStart).trim().replace(/calc\(\s*([\d.]+)\s*\*\s*var\(--shadow-intensity\)\s*\)/, "$1");
168
- const dims = s.slice(0, colorStart).trim().split(/\s+/);
169
- if (dims.length < 3 || dims.length > 4) throw new Error(`shadow layer wants 3-4 lengths (offsetX offsetY blur [spread]): ${JSON.stringify(s)}`);
170
- const dim = (d) => (d === "0" ? { value: 0, unit: "px" } : parseDimensionCss(d));
171
- return {
172
- color: parseColorCss(colorCss),
173
- offsetX: dim(dims[0]),
174
- offsetY: dim(dims[1]),
175
- blur: dim(dims[2]),
176
- spread: dims[3] !== undefined ? dim(dims[3]) : { value: 0, unit: "px" },
177
- };
178
- });
179
- }
180
-
181
- // Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
182
- function parseTrackingCss(css) {
183
- const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
184
- if (!m) throw new Error(`unparseable tracking: ${JSON.stringify(css)}`);
185
- return parseFloat(m[1]);
186
- }
187
-
188
- // Parse "cubic-bezier(x1,y1,x2,y2)" into its four numbers.
189
- function parseCubicBezierCss(css) {
190
- const m = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/.exec(css.trim());
191
- if (!m) throw new Error(`unparseable cubic-bezier: ${JSON.stringify(css)}`);
192
- return [1, 2, 3, 4].map((i) => parseFloat(m[i]));
193
- }
194
-
195
- // Parse a font stack string into its array of unquoted family names.
196
- function parseFontStackCss(css) {
197
- return splitTopLevel(css, ",").map((p) => unquote(p)).filter((p) => p !== "");
198
- }
199
-
200
- // Parse a ramp letterSpacing string ("+0.02em" | "-0.28px" | "0") into an em multiplier, px scaled by fontSize.
201
- function parseLetterSpacingCss(css, fontSizePx) {
202
- const m = /^([+-]?\d*\.?\d+)(em|px)?$/.exec(css.trim());
203
- if (!m) throw new Error(`unparseable letterSpacing: ${JSON.stringify(css)}`);
204
- const n = parseFloat(m[1]);
205
- return m[2] === "px" ? round5(n / fontSizePx) : n;
206
- }
207
-
208
- function parseDimensionCss(css) {
209
- const m = /^(-?\d*\.?\d+)px$/.exec(css.trim());
210
- if (!m) throw new Error(`unparseable dimension: ${JSON.stringify(css)}`);
211
- return { value: parseFloat(m[1]), unit: "px" };
212
- }
213
-
214
- function parseDurationCss(css) {
215
- const m = /^(\d*\.?\d+)ms$/.exec(css.trim());
216
- if (!m) throw new Error(`unparseable duration: ${JSON.stringify(css)}`);
217
- return { value: parseFloat(m[1]), unit: "ms" };
218
- }
219
-
220
- function parseNumberCss(css) {
221
- const n = parseFloat(css.trim());
222
- if (Number.isNaN(n) || !/^[+-]?\d*\.?\d+$/.test(css.trim())) throw new Error(`unparseable number: ${JSON.stringify(css)}`);
223
- return n;
224
- }
225
-
226
- // ---------------------------------------------------------------------------
227
- // Serialization — the schema's deterministic byte forms for tokens without preserved .css
228
- // ---------------------------------------------------------------------------
229
-
230
- // Serialize a concrete $value to its deterministic CSS byte form (the fallback when .css is absent).
231
- function serializeValue(type, value) {
232
- if (type === "dimension" || type === "duration") return `${value.value}${value.unit}`;
233
- if (type === "number" || type === "fontWeight") return String(value);
234
- if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
235
- if (type === "fontFamily") {
236
- return (Array.isArray(value) ? value : [value]).map((f) => (/\s/.test(f) ? `"${f}"` : f)).join(", ");
237
- }
238
- if (type === "color") {
239
- if (value.colorSpace === "srgb" && value.hex) return value.hex;
240
- return `oklch(${value.components.join(" ")}${value.alpha !== undefined ? ` / ${value.alpha}` : ""})`;
241
- }
242
- if (type === "shadow") {
243
- // Deterministic fallback (the migration always preserves .css): zero lengths serialize bare "0";
244
- // the layer alpha rides the master multiplier so --shadow-intensity keeps flattening shadows.
245
- const dim = (d) => (d.value === 0 ? "0" : `${d.value}${d.unit}`);
246
- const layerColor = (c) => {
247
- const alpha = c.alpha ?? 1;
248
- if (c.colorSpace === "srgb") {
249
- const bytes = c.components.map((n) => Math.round(n * 255));
250
- return `rgb(${bytes.join(" ")} / calc(${alpha} * var(--shadow-intensity)))`;
251
- }
252
- return `oklch(${c.components.join(" ")} / calc(${alpha} * var(--shadow-intensity)))`;
253
- };
254
- return value.map((l) => `${dim(l.offsetX)} ${dim(l.offsetY)} ${dim(l.blur)} ${dim(l.spread ?? { value: 0, unit: "px" })} ${layerColor(l.color)}`).join(", ");
255
- }
256
- if (type === "gradient") {
257
- // Deterministic fallback (authored washes preserve .css): a top-down tint ramp from the stop list.
258
- const stops = value.map((s) => `${serializeValue("color", s.color)} ${Math.round(s.position * 100)}%`).join(", ");
259
- return `linear-gradient(180deg, ${stops})`;
260
- }
261
- return String(value);
262
- }
263
-
264
- // ---------------------------------------------------------------------------
265
- // Artifact loading & resolution (SCHEMA.md resolver section — deterministic order)
266
- // ---------------------------------------------------------------------------
267
-
268
- // True when a $value is an alias reference string ("{path.to.token}").
269
- function isAlias(v) {
270
- return typeof v === "string" && /^\{[^}]+\}$/.test(v);
271
- }
272
-
273
- // True when a node is a token (an object carrying $type).
274
- function isToken(node) {
275
- return node !== null && typeof node === "object" && !Array.isArray(node) && "$type" in node;
276
- }
277
-
278
- // Collect every token in a tree as a path → token map ($-prefixed keys are metadata, not groups).
279
- function collectTokens(root) {
280
- const tokens = new Map();
281
- const walk = (node, path) => {
282
- if (isToken(node)) { tokens.set(path.join("."), node); return; }
283
- if (node !== null && typeof node === "object" && !Array.isArray(node)) {
284
- for (const [k, v] of Object.entries(node)) if (!k.startsWith("$")) walk(v, [...path, k]);
285
- }
286
- };
287
- walk(root, []);
288
- return tokens;
289
- }
290
-
291
- // Overlay one modifier file onto the base at token-path granularity; an overlay may only
292
- // override paths that exist in the base, never introduce new ones.
293
- function overlayTree(base, overlay, file, errors, path = []) {
294
- for (const [key, value] of Object.entries(overlay)) {
295
- if (key.startsWith("$")) continue;
296
- const here = [...path, key];
297
- if (!(key in base)) {
298
- errors.push(`rule 1 — resolver overlay ${file}: introduces ${here.join(".")}, absent from the base set`);
299
- continue;
300
- }
301
- if (isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
302
- base[key] = value;
303
- } else {
304
- overlayTree(base[key], value, file, errors, here);
305
- }
306
- }
307
- }
308
-
309
- // Concatenate a later set file onto the accumulated base (a later file overrides per token path; new paths are legal here).
310
- function concatTree(base, add) {
311
- for (const [key, value] of Object.entries(add)) {
312
- if (key.startsWith("$")) continue;
313
- if (!(key in base) || isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
314
- base[key] = value;
315
- } else {
316
- concatTree(base[key], value);
317
- }
318
- }
319
- }
320
-
321
- // Load and resolve one artifact directory: parse files (rule 1), concatenate sets, build one
322
- // resolved view per theme context. Returns null when the artifact is unreadable.
323
- function loadArtifact(dir, errors) {
324
- const readJson = (name) => {
325
- const path = join(dir, name);
326
- if (!existsSync(path)) { errors.push(`rule 1 — ${name}: missing`); return null; }
327
- try {
328
- return JSON.parse(readFileSync(path, "utf8"));
329
- } catch (e) {
330
- errors.push(`rule 1 — ${name}: unparseable JSON (${e.message})`);
331
- return null;
332
- }
333
- };
334
-
335
- const base = readJson("design.tokens.json");
336
- const resolver = readJson("design.resolver.json");
337
- if (base === null || resolver === null) return null;
338
-
339
- if (resolver.schema !== "design-resolver/v1") {
340
- errors.push(`rule 1 — design.resolver.json: schema must be "design-resolver/v1" (found ${JSON.stringify(resolver.schema)})`);
341
- }
342
- if (!Array.isArray(resolver.sets) || resolver.sets.length === 0) {
343
- errors.push("rule 1 — design.resolver.json: sets must be a non-empty array");
344
- return null;
345
- }
346
- const themeModifier = (resolver.modifiers ?? []).find((m) => m?.name === "theme");
347
- for (const m of resolver.modifiers ?? []) {
348
- if (m?.name !== "theme") errors.push(`rule 1 — design.resolver.json: unknown modifier ${JSON.stringify(m?.name)} (exactly one modifier today: theme)`);
349
- }
350
- if (!themeModifier) {
351
- errors.push("rule 1 — design.resolver.json: missing the theme modifier");
352
- return null;
353
- }
354
- if (!LEGAL_THEMES.includes(themeModifier.default)) errors.push(`rule 1 — design.resolver.json: theme default must be light|dark (found ${JSON.stringify(themeModifier.default)}); it mirrors the payload's theme-default`);
355
- if (!Array.isArray(themeModifier.contexts?.light) || themeModifier.contexts.light.length !== 0) {
356
- errors.push("rule 1 — design.resolver.json: the light context must be the empty overlay [] (the base set is the light rendering)");
357
- }
358
-
359
- // Every file the resolver references exists beside it — and parses.
360
- const files = new Map([["design.tokens.json", base]]);
361
- const referenced = new Set(["design.tokens.json"]);
362
- const loadReferenced = (name, where) => {
363
- referenced.add(name);
364
- if (files.has(name)) return;
365
- const doc = readJson(name);
366
- if (doc === null) errors.push(`rule 1 — resolver ${where} references ${name}`);
367
- else files.set(name, doc);
368
- };
369
- for (const set of resolver.sets) {
370
- for (const name of set?.values ?? []) loadReferenced(name, `set "${set.name}"`);
371
- }
372
- for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
373
- for (const name of names ?? []) loadReferenced(name, `theme context "${context}"`);
374
- }
375
-
376
- // The dark file exists exactly when the resolver's dark context names it.
377
- const darkNamed = (themeModifier.contexts?.dark ?? []).includes("design.tokens.dark.json");
378
- const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
379
- if (darkNamed && !darkExists) errors.push("rule 1 — design.tokens.dark.json: named by the resolver's dark context but absent");
380
- if (!darkNamed && darkExists) errors.push("rule 1 — design.tokens.dark.json: present but not named by a resolver dark context");
381
-
382
- // Resolution: concatenate the set files in array order, then one view per theme context.
383
- const merged = {};
384
- for (const set of resolver.sets) {
385
- for (const name of set?.values ?? []) {
386
- if (files.has(name)) concatTree(merged, files.get(name));
387
- }
388
- }
389
- const views = new Map();
390
- for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
391
- const view = structuredClone(merged);
392
- for (const name of names ?? []) {
393
- if (files.has(name)) overlayTree(view, files.get(name), name, errors);
394
- }
395
- views.set(context, view);
396
- }
397
-
398
- return { base, resolver, files, views, payload: base?.$extensions?.[EXT] };
399
- }
400
-
401
- // Follow an alias chain to its final concrete token; records rule-6 errors (dangling refs, cycles, type mismatches).
402
- function resolveAlias(ref, tokens, expectedType, context, errors) {
403
- const seen = new Set();
404
- let cur = ref;
405
- while (isAlias(cur)) {
406
- const path = cur.slice(1, -1);
407
- if (seen.has(path)) { errors.push(`rule 6 — ${context}: alias cycle through {${path}}`); return null; }
408
- seen.add(path);
409
- const target = tokens.get(path);
410
- if (!target) { errors.push(`rule 6 — ${context}: alias {${path}} does not resolve`); return null; }
411
- if (expectedType && target.$type !== expectedType) {
412
- errors.push(`rule 6 — ${context}: alias {${path}} resolves to $type ${target.$type} (want ${expectedType})`);
413
- return null;
414
- }
415
- if (!isAlias(target.$value)) return target;
416
- cur = target.$value;
417
- }
418
- return null;
419
- }
420
-
421
- // The emitted CSS string for a token path: follow aliases, prefer the preserved .css, fall back
422
- // to the schema's deterministic serialization.
423
- function cssFor(path, tokens, errors) {
424
- const token = tokens.get(path);
425
- if (!token) { errors.push(`emit — ${path}: token not found`); return ""; }
426
- let target = token;
427
- if (isAlias(token.$value)) {
428
- target = resolveAlias(token.$value, tokens, token.$type, path, errors);
429
- if (!target) return "";
430
- }
431
- const css = target.$extensions?.[EXT]?.css;
432
- if (typeof css === "string") return css;
433
- return serializeValue(target.$type, target.$value);
434
- }
435
-
436
- // ---------------------------------------------------------------------------
437
- // Validation — SCHEMA.md rules 1–12 (rule 12 is enforced at emit time)
438
- // ---------------------------------------------------------------------------
439
-
440
- // True when a token's $value is absent or vacuously empty.
441
- function isEmptyValue(v) {
442
- return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
443
- }
444
-
445
- // Verify a preserved-css color token: parse-back equality plus the srgb hex round-trip.
446
- function verifyColorParseBack(name, token, errors) {
447
- const css = token.$extensions?.[EXT]?.css;
448
- if (typeof css !== "string" || isAlias(token.$value)) return;
449
- let reparsed;
450
- try { reparsed = parseColorCss(css); } catch (e) { errors.push(`rule 9 — ${name}: ${e.message}`); return; }
451
- if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) {
452
- errors.push(`rule 9 — ${name}: parse-back mismatch — css ${JSON.stringify(css)} → ${JSON.stringify(reparsed)} != $value ${JSON.stringify(token.$value)}`);
453
- return;
454
- }
455
- if (token.$value.colorSpace === "srgb") {
456
- const viaHex = parseColorCss(token.$value.hex);
457
- if (JSON.stringify(viaHex.components) !== JSON.stringify(token.$value.components)) {
458
- errors.push(`rule 9 — ${name}: srgb components do not round-trip through hex ${token.$value.hex}`);
459
- }
460
- }
461
- }
462
-
463
- // Rule 9 for one non-color token carrying a preserved string .css: parsing it must reproduce $value.
464
- function verifyScalarParseBack(name, token, css, errors) {
465
- if (isAlias(token.$value)) return; // an aliased token takes the target's .css; rule 6 owns alias integrity
466
- try {
467
- if (token.$type === "cubicBezier") {
468
- if (JSON.stringify(parseCubicBezierCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
469
- } else if (token.$type === "fontFamily") {
470
- if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
471
- } else if (token.$type === "number") {
472
- if (parseTrackingCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
473
- } else if (token.$type === "dimension") {
474
- if (JSON.stringify(parseDimensionCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
475
- } else if (token.$type === "duration") {
476
- if (JSON.stringify(parseDurationCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
477
- } else if (token.$type === "fontWeight") {
478
- if (parseNumberCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
479
- }
480
- } catch (e) {
481
- errors.push(`rule 9 — ${name}: ${e.message}`);
482
- }
483
- }
484
-
485
- // Run the full coverage gate (rules 1–15) over one artifact directory; returns everything emit needs.
486
- // requestedTheme null = boot theme follows the payload's theme-default (light when absent).
487
- function runCheck(dir, requestedTheme) {
488
- const errors = [];
489
- const artifact = loadArtifact(dir, errors);
490
- if (!artifact) return { errors };
491
- const { base, resolver, files, views, payload } = artifact;
492
-
493
- // Rule 2 — the file-level payload on the base root.
494
- if (!payload || typeof payload !== "object") {
495
- errors.push(`rule 2 — payload: missing $extensions["${EXT}"] on the base file's root group`);
496
- return { errors };
497
- }
498
- if (payload.schema !== "design-tokens/v2") errors.push(`rule 2 — payload.schema: must be "design-tokens/v2" (found ${JSON.stringify(payload.schema)})`);
499
- if (payload.dtcg !== "2025.10") errors.push(`rule 2 — payload.dtcg: must be "2025.10" (found ${JSON.stringify(payload.dtcg)})`);
500
- if (typeof payload.product !== "string" || payload.product.trim() === "") errors.push("rule 2 — payload.product: missing or empty");
501
- if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`rule 2 — payload.provenance: must be one of ${LEGAL_PROVENANCE.join("|")} (found ${JSON.stringify(payload.provenance)})`);
502
- if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.sourced ?? "")) errors.push(`rule 2 — payload.sourced: must be an ISO date (found ${JSON.stringify(payload.sourced)})`);
503
- if (!LEGAL_VERBOSITY.includes(payload.verbosity)) errors.push(`rule 2 — payload.verbosity: must be one of ${LEGAL_VERBOSITY.join("|")} (found ${JSON.stringify(payload.verbosity)})`);
504
- if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`rule 2 — payload.feedback: must be one of ${LEGAL_FEEDBACK.join("|")} (found ${JSON.stringify(payload.feedback)})`);
505
- if (!LEGAL_DENSITY.includes(payload["density-tier"])) errors.push(`rule 2 — payload.density-tier: must be one of ${LEGAL_DENSITY.join("|")} (found ${JSON.stringify(payload["density-tier"])})`);
506
- if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("rule 2 — payload.product-traits: must be a non-empty array");
507
- if (payload["theme-default"] !== undefined && !LEGAL_THEMES.includes(payload["theme-default"])) {
508
- errors.push(`rule 2 — payload.theme-default: must be light|dark when present (found ${JSON.stringify(payload["theme-default"])})`);
509
- }
510
- const themeDefault = LEGAL_THEMES.includes(payload["theme-default"]) ? payload["theme-default"] : "light";
511
- const resolverDefault = (resolver.modifiers ?? []).find((m) => m?.name === "theme")?.default;
512
- if (LEGAL_THEMES.includes(resolverDefault) && resolverDefault !== themeDefault) {
513
- errors.push(`rule 2 — design.resolver.json: theme default ${JSON.stringify(resolverDefault)} does not mirror payload theme-default ${JSON.stringify(themeDefault)}`);
514
- }
515
- const anchors = payload.anchors;
516
- const anchorsShapeOk = Array.isArray(anchors) && anchors.length >= 2 && anchors.length <= 3 && anchors.every((a) => typeof a === "string" && a.trim() !== "");
517
- if (anchors !== undefined && !anchorsShapeOk) errors.push(`rule 2 — payload.anchors: must be an array of 2–3 non-empty strings when present (found ${JSON.stringify(anchors)})`);
518
- if (payload.provenance === "project" && anchors === undefined) errors.push("rule 2 — payload.anchors: required on provenance: project artifacts (the named real-product anchors from intake)");
519
- // Rule 2 (look) — the optional persisted intake look block (SCHEMA.md, the payload): enum-checked
520
- // when present; theme-default and anchors stay the canonical top-level projections emission consumes.
521
- const look = payload.look;
522
- if (look !== undefined) {
523
- if (look === null || typeof look !== "object" || Array.isArray(look)) {
524
- errors.push("rule 2 — payload.look: must be an object when present (the persisted intake look block)");
525
- } else {
526
- if (!LOOK_THEMES.includes(look.theme)) errors.push(`rule 2 — payload.look.theme: must be one of ${LOOK_THEMES.join("|")} (found ${JSON.stringify(look.theme)})`);
527
- if (!LOOK_DELIGHTS.includes(look.delight)) errors.push(`rule 2 — payload.look.delight: must be one of ${LOOK_DELIGHTS.join("|")} (found ${JSON.stringify(look.delight)})`);
528
- if (!LOOK_RESOLUTION_MODES.includes(look["resolution-mode"])) errors.push(`rule 2 — payload.look.resolution-mode: must be one of ${LOOK_RESOLUTION_MODES.join("|")} (found ${JSON.stringify(look["resolution-mode"])})`);
529
- const lookAnchors = look.anchors;
530
- if (!Array.isArray(lookAnchors) || lookAnchors.length < 2 || lookAnchors.length > 3 || !lookAnchors.every((a) => typeof a === "string" && a.trim() !== "")) {
531
- errors.push(`rule 2 — payload.look.anchors: must be an array of 2–3 non-empty named strings (found ${JSON.stringify(lookAnchors)})`);
532
- }
533
- if (typeof look.resolution !== "string" || !LOOK_RESOLUTION_RE.test(look.resolution.trim())) {
534
- errors.push(`rule 2 — payload.look.resolution: must match project | library:<slug> | seed:<style>+<palette> | default (found ${JSON.stringify(look.resolution)})`);
535
- }
536
- }
537
- }
538
- const lm = payload["lever-manifest"];
539
- for (const key of MANIFEST_KEYS) {
540
- if (lm?.[key] === undefined) errors.push(`rule 2 — payload.lever-manifest.${key}: missing`);
541
- }
542
-
543
- const theme = requestedTheme ?? themeDefault;
544
- if (!views.has(theme)) {
545
- errors.push(`rule 1 — resolver: theme ${theme} selected but the resolver carries no ${theme} context`);
546
- return { errors };
547
- }
548
-
549
- const lightView = views.get("light");
550
- const lightTokens = collectTokens(lightView);
551
-
552
- // Rule 3 — all 27 primitives present with the exact $type, each assigned or hinted.
553
- for (const [key, type] of PRIMITIVE_TYPES) {
554
- const t = lightView.primitive?.[key];
555
- if (!isToken(t)) { errors.push(`rule 3 — primitive.${key}: missing`); continue; }
556
- if (t.$type !== type) errors.push(`rule 3 — primitive.${key}: $type ${JSON.stringify(t.$type)} (want ${type})`);
557
- if (isEmptyValue(t.$value)) errors.push(`rule 3 — primitive.${key}: empty $value`);
558
- if (key === "stagger-unit" && typeof t.$value?.value === "number" && (t.$value.value < 0 || t.$value.value > 60)) {
559
- errors.push(`rule 3 — primitive.stagger-unit: ${t.$value.value}ms outside the legal 0–60ms range`);
560
- }
561
- }
562
-
563
- // Rule 4 — all 36 color roles present, typed color, legal colorSpace, .css present (via alias when aliased).
564
- for (const role of ALL_TOKENS) {
565
- const t = lightView.color?.[role];
566
- if (!isToken(t)) { errors.push(`rule 4 — color.${role}: missing`); continue; }
567
- if (t.$type !== "color") { errors.push(`rule 4 — color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
568
- const target = isAlias(t.$value) ? resolveAlias(t.$value, lightTokens, "color", `color.${role}`, []) : t;
569
- if (!target) continue; // dangling alias — rule 6 reports it
570
- if (!["oklch", "srgb"].includes(target.$value?.colorSpace)) errors.push(`rule 4 — color.${role}: colorSpace ${JSON.stringify(target.$value?.colorSpace)} (want oklch|srgb)`);
571
- if (typeof target.$extensions?.[EXT]?.css !== "string") errors.push(`rule 4 — color.${role}: missing preserved .css`);
572
- }
573
-
574
- // Rule 5 — typography coverage: the three families (display optional) and the seven-step ramp with all five sub-values.
575
- for (const fam of ["heading", "body", "mono"]) {
576
- const t = lightView.typography?.family?.[fam];
577
- if (!isToken(t) || t.$type !== "fontFamily") errors.push(`rule 5 — typography.family.${fam}: missing or not $type fontFamily`);
578
- }
579
- const famDisplay = lightView.typography?.family?.display;
580
- if (famDisplay !== undefined && (!isToken(famDisplay) || famDisplay.$type !== "fontFamily")) {
581
- errors.push("rule 5 — typography.family.display: not $type fontFamily (the slot is optional; its shape is not)");
582
- }
583
- for (const step of RAMP_STEPS) {
584
- const t = lightView.typography?.ramp?.[step];
585
- if (!isToken(t) || t.$type !== "typography") { errors.push(`rule 5 — typography.ramp.${step}: missing or not $type typography`); continue; }
586
- for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
587
- if (t.$value?.[sub] === undefined) errors.push(`rule 5 — typography.ramp.${step}.${sub}: missing sub-value`);
588
- }
589
- }
590
-
591
- // Rule 6 — alias integrity over every resolved context view (aliases resolve after overlay).
592
- const SUB_TYPES = { fontFamily: "fontFamily", fontWeight: "fontWeight", letterSpacing: "number", fontSize: "dimension", lineHeight: "number" };
593
- for (const [context, view] of views) {
594
- const tokens = collectTokens(view);
595
- const label = context === "light" ? "" : ` [${context}]`;
596
- for (const [path, token] of tokens) {
597
- if (isAlias(token.$value)) resolveAlias(token.$value, tokens, token.$type, `${path}${label}`, errors);
598
- if (token.$type === "typography" && token.$value && typeof token.$value === "object") {
599
- for (const [sub, want] of Object.entries(SUB_TYPES)) {
600
- const v = token.$value[sub];
601
- if (isAlias(v)) resolveAlias(v, tokens, want, `${path}.${sub}${label}`, errors);
602
- }
603
- }
604
- }
605
- }
606
-
607
- // Rule 7 — manifest scales.
608
- if (lm && typeof lm === "object") {
609
- const ascending = (arr) => arr.every((v, i) => i === 0 || v > arr[i - 1]);
610
- for (const key of ["spacing-scale", "radius-scale"]) {
611
- if (!Array.isArray(lm[key]) || lm[key].length === 0 || !ascending(lm[key])) errors.push(`rule 7 — lever-manifest.${key}: must be non-empty ascending (found ${JSON.stringify(lm[key])})`);
612
- }
613
- if (!Array.isArray(lm.motion?.durations) || lm.motion.durations.length === 0 || !ascending(lm.motion.durations)) errors.push(`rule 7 — lever-manifest.motion.durations: must be non-empty ascending (found ${JSON.stringify(lm.motion?.durations)})`);
614
- if (!Array.isArray(lm.motion?.easings) || lm.motion.easings.length === 0) errors.push("rule 7 — lever-manifest.motion.easings: must be non-empty");
615
- const shadows = lm["shadow-levels"];
616
- const ranks = (Array.isArray(shadows) ? shadows : []).map((s) => SHADOW_RANK[s]);
617
- if (ranks.length === 0 || ranks.some((r) => r === undefined) || !ascending(ranks)) {
618
- errors.push(`rule 7 — lever-manifest.shadow-levels: must be non-empty, drawn from ${Object.keys(SHADOW_RANK).join(" ")}, strictly increasing (found ${JSON.stringify(shadows)})`);
619
- }
620
- if (typeof lm.icons?.family !== "string" || lm.icons.family === "") errors.push("rule 7 — lever-manifest.icons.family: missing or empty");
621
- if (!(lm["accent-count"] >= 1)) errors.push(`rule 7 — lever-manifest.accent-count: must be >= 1 (found ${JSON.stringify(lm?.["accent-count"])})`);
622
-
623
- // Rule 8 — the look's own base values sit on its own allowlist. An off-manifest value is legal
624
- // only as a recorded deviation: its $description must open with the deviation: layer tag (rule 15).
625
- const deviationTagged = (t) => typeof t?.$description === "string" && t.$description.startsWith("deviation:");
626
- const radiusToken = lightView.primitive?.radius;
627
- const radius = radiusToken?.$value?.value;
628
- if (Array.isArray(lm["radius-scale"]) && radius !== undefined && !lm["radius-scale"].includes(radius) && !deviationTagged(radiusToken)) {
629
- errors.push(`rule 8 — primitive.radius: ${radius} not on lever-manifest.radius-scale (off-manifest values need a deviation: $description)`);
630
- }
631
- for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
632
- const t = lightView.primitive?.[key];
633
- const ms = t?.$value?.value;
634
- if (Array.isArray(lm.motion?.durations) && ms !== undefined && !lm.motion.durations.includes(ms) && !deviationTagged(t)) {
635
- errors.push(`rule 8 — primitive.${key}: ${ms} not in lever-manifest.motion.durations (off-manifest values need a deviation: $description)`);
636
- }
637
- }
638
- const ease = lightView.primitive?.["ease-default"];
639
- if (isToken(ease) && Array.isArray(lm.motion?.easings)) {
640
- const easeCss = typeof ease.$extensions?.[EXT]?.css === "string" ? ease.$extensions[EXT].css : serializeValue("cubicBezier", ease.$value);
641
- if (!lm.motion.easings.includes(easeCss) && !deviationTagged(ease)) errors.push(`rule 8 — primitive.ease-default: ${JSON.stringify(easeCss)} not in lever-manifest.motion.easings (off-manifest values need a deviation: $description)`);
642
- }
643
- }
644
-
645
- // Rule 9 — parse-back for every token carrying a preserved .css, per file.
646
- for (const [fileName, doc] of files) {
647
- const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
648
- for (const [path, token] of collectTokens(doc)) {
649
- const css = token.$extensions?.[EXT]?.css;
650
- if (css === undefined) continue;
651
- const name = `${prefix}${path}`;
652
- if (token.$type === "color") {
653
- verifyColorParseBack(name, token, errors);
654
- } else if (token.$type === "shadow" && typeof css === "string") {
655
- try {
656
- const reparsed = parseShadowCss(css);
657
- if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: shadow css ${JSON.stringify(css)} does not parse back to $value`);
658
- } catch (e) {
659
- errors.push(`rule 9 — ${name}: ${e.message}`);
660
- }
661
- } else if (token.$type === "gradient" && typeof css === "string") {
662
- // A gradient's .css carries geometry the stop list does not; verify each stop color's byte form appears in it.
663
- for (const [i, stop] of (Array.isArray(token.$value) ? token.$value : []).entries()) {
664
- if (stop?.color && typeof stop.color === "object" && !css.includes(serializeValue("color", stop.color))) {
665
- errors.push(`rule 9 — ${name}: stop ${i} color ${serializeValue("color", stop.color)} not present in the preserved .css`);
666
- }
667
- }
668
- } else if (typeof css === "string") {
669
- verifyScalarParseBack(name, token, css, errors);
670
- } else if (token.$type === "typography" && typeof css === "object" && css !== null) {
671
- if (typeof css.letterSpacing === "string" && typeof token.$value?.letterSpacing === "number" && typeof token.$value?.fontSize?.value === "number") {
672
- try {
673
- const reparsed = parseLetterSpacingCss(css.letterSpacing, token.$value.fontSize.value);
674
- if (reparsed !== token.$value.letterSpacing) errors.push(`rule 9 — ${name}: letterSpacing css ${css.letterSpacing} → ${reparsed} != ${token.$value.letterSpacing}`);
675
- } catch (e) {
676
- errors.push(`rule 9 — ${name}: ${e.message}`);
677
- }
678
- }
679
- }
680
- }
681
- }
682
-
683
- // Rule 10 — the dark file, when present, carries exactly the 36-role color group and nothing else.
684
- const darkDoc = files.get("design.tokens.dark.json");
685
- if (darkDoc !== undefined) {
686
- const rootKeys = Object.keys(darkDoc).filter((k) => !k.startsWith("$"));
687
- if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`rule 10 — design.tokens.dark.json: root groups ${JSON.stringify(rootKeys)} (want exactly ["color"])`);
688
- if (darkDoc.$extensions?.[EXT] !== undefined) errors.push("rule 10 — design.tokens.dark.json: carries a payload (the base file is the payload's single carrier)");
689
- for (const role of ALL_TOKENS) {
690
- const t = darkDoc.color?.[role];
691
- if (!isToken(t)) { errors.push(`rule 10 — dark color.${role}: missing`); continue; }
692
- if (t.$type !== "color") { errors.push(`rule 10 — dark color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
693
- if (isAlias(t.$value)) continue; // resolves post-overlay; rules 4/6 cover it on the dark view
694
- if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`rule 10 — dark color.${role}: colorSpace ${JSON.stringify(t.$value?.colorSpace)} (want oklch|srgb)`);
695
- if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 10 — dark color.${role}: missing preserved .css`);
696
- }
697
- for (const role of Object.keys(darkDoc.color ?? {})) {
698
- if (!ALL_TOKENS.includes(role)) errors.push(`rule 10 — dark color.${role}: not a legal role`);
699
- }
700
- }
701
-
702
- // Rule 11 — only the ten legal $type values appear, in any file.
703
- for (const [fileName, doc] of files) {
704
- const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
705
- for (const [path, token] of collectTokens(doc)) {
706
- if (!LEGAL_TYPES.includes(token.$type)) errors.push(`rule 11 — ${prefix}${path}: illegal $type ${JSON.stringify(token.$type)}`);
707
- }
708
- }
709
-
710
- // True when a shadow/wash layer color is a legal DTCG color value; records rule errors under `where`.
711
- const checkLayerColor = (color, where, rule) => {
712
- if (!color || typeof color !== "object" || !["oklch", "srgb"].includes(color.colorSpace) || !Array.isArray(color.components)) {
713
- errors.push(`${rule} — ${where}: color must be a typed oklch|srgb value (found ${JSON.stringify(color)})`);
714
- return;
715
- }
716
- const isPureBlack = color.colorSpace === "srgb"
717
- ? color.components.every((c) => c === 0)
718
- : color.components[0] === 0 && color.components[1] === 0;
719
- if (rule === "rule 13" && isPureBlack && (color.alpha ?? 1) >= 0.15) {
720
- errors.push(`${rule} — ${where}: pure black at alpha ${color.alpha ?? 1} (≥ 0.15) — shadow colors are brand-tintable, never a hard black drop`);
721
- }
722
- };
723
-
724
- // Rule 13 — elevation coverage: all four elevation.* present, $type shadow, ≥ 1 layer each,
725
- // every layer shape-legal and rule-conformant, the byte form preserved; each assigned or hinted.
726
- for (const level of ELEVATION_TOKENS) {
727
- const t = lightView.elevation?.[level];
728
- if (!isToken(t)) { errors.push(`rule 13 — elevation.${level}: missing`); continue; }
729
- if (t.$type !== "shadow") { errors.push(`rule 13 — elevation.${level}: $type ${JSON.stringify(t.$type)} (want shadow)`); continue; }
730
- if (!Array.isArray(t.$value) || t.$value.length === 0) { errors.push(`rule 13 — elevation.${level}: $value must be a non-empty array of layers`); continue; }
731
- for (const [i, layer] of t.$value.entries()) {
732
- checkLayerColor(layer?.color, `elevation.${level} layer ${i}`, "rule 13");
733
- for (const dim of ["offsetX", "offsetY", "blur"]) {
734
- if (typeof layer?.[dim]?.value !== "number") errors.push(`rule 13 — elevation.${level} layer ${i}: ${dim} must be a dimension`);
735
- }
736
- }
737
- if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 13 — elevation.${level}: missing preserved .css (the serialized shadow list)`);
738
- }
739
- for (const level of Object.keys(lightView.elevation ?? {})) {
740
- if (!level.startsWith("$") && !ELEVATION_TOKENS.includes(level)) errors.push(`rule 13 — elevation.${level}: not a legal level (sm|md|lg|overlay)`);
741
- }
742
-
743
- // Rule 14 — wash shape: the group is optional; when present, ≤ 2 gradient tokens named
744
- // surface/emphasis with rule-conformant stops. Presence is never required.
745
- if (lightView.wash !== undefined) {
746
- const washKeys = Object.keys(lightView.wash).filter((k) => !k.startsWith("$"));
747
- if (washKeys.length > 2) errors.push(`rule 14 — wash: at most 2 tokens (found ${washKeys.length})`);
748
- for (const key of washKeys) {
749
- if (!WASH_TOKENS.includes(key)) { errors.push(`rule 14 — wash.${key}: not a legal wash slot (surface|emphasis)`); continue; }
750
- const t = lightView.wash[key];
751
- if (!isToken(t) || t.$type !== "gradient") { errors.push(`rule 14 — wash.${key}: must be $type gradient`); continue; }
752
- if (!Array.isArray(t.$value) || t.$value.length < 2) { errors.push(`rule 14 — wash.${key}: $value must be an array of ≥ 2 stops`); continue; }
753
- for (const [i, stop] of t.$value.entries()) {
754
- checkLayerColor(stop?.color, `wash.${key} stop ${i}`, "rule 14");
755
- if (typeof stop?.position !== "number" || stop.position < 0 || stop.position > 1) errors.push(`rule 14 — wash.${key} stop ${i}: position must be a number in 0–1`);
756
- }
757
- if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 14 — wash.${key}: missing preserved .css (the authored gradient)`);
758
- }
759
- }
760
-
761
- // Rule 15 — provenance vocabulary: a per-value $description opening with a tag must use a legal
762
- // one (source:/brief:/deviation: are the layer tags; untagged = inherited). Rule 8 enforces the
763
- // off-manifest side (every off-manifest value carries deviation:).
764
- for (const [fileName, doc] of files) {
765
- const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
766
- for (const [path, token] of collectTokens(doc)) {
767
- const m = typeof token.$description === "string" && /^([a-z][a-z-]*):/.exec(token.$description);
768
- if (m && !LEGAL_DESCRIPTION_TAGS.includes(m[1])) {
769
- errors.push(`rule 15 — ${prefix}${path}: $description opens with unknown tag "${m[1]}:" (layer tags: source:|brief:|deviation:; untagged = inherited)`);
770
- }
771
- }
772
- }
773
-
774
- // Rule 12 inventory — every token still carrying a hint (passes --check; --emit refuses).
775
- const hints = [];
776
- for (const [fileName, doc] of files) {
777
- const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
778
- for (const [path, token] of collectTokens(doc)) {
779
- const hint = token.$extensions?.[EXT]?.hint;
780
- if (hint !== undefined) hints.push({ key: `${prefix}${path}`, text: String(hint) });
781
- }
782
- }
783
-
784
- return { errors, payload, views, hints, theme, hasDark: files.has("design.tokens.dark.json"), hasWash: lightView.wash !== undefined };
785
- }
786
-
787
- // ---------------------------------------------------------------------------
788
- // Emitter feed — flatten the resolved views into the value map the emitters consume
789
- // ---------------------------------------------------------------------------
790
-
791
- // Assemble the emitters' document: 27 primitive CSS strings + the payload dials, the 36-role CSS
792
- // string map per theme, the elevation/wash value strings, and the ramp steps as emitter-ready data
793
- // (preferring each token's preserved .css throughout).
794
- function buildEmitterDoc(views, payload, errors) {
795
- const lightView = views.get("light");
796
- const lightTokens = collectTokens(lightView);
797
- const primitives = {};
798
- for (const key of PRIMITIVES) primitives[key] = cssFor(`primitive.${key}`, lightTokens, errors);
799
- primitives.verbosity = payload.verbosity;
800
- primitives.feedback = payload.feedback;
801
- primitives["density-tier"] = payload["density-tier"];
802
-
803
- const colorTokens = { light: {} };
804
- for (const role of ALL_TOKENS) colorTokens.light[role] = cssFor(`color.${role}`, lightTokens, errors);
805
-
806
- if (views.has("dark")) {
807
- const darkTokens = collectTokens(views.get("dark"));
808
- colorTokens.dark = {};
809
- for (const role of ALL_TOKENS) colorTokens.dark[role] = cssFor(`color.${role}`, darkTokens, errors);
810
- }
811
-
812
- const elevation = {};
813
- for (const level of ELEVATION_TOKENS) elevation[level] = cssFor(`elevation.${level}`, lightTokens, errors);
814
-
815
- let wash = null;
816
- if (lightView.wash !== undefined) {
817
- wash = {};
818
- for (const key of WASH_TOKENS) {
819
- if (isToken(lightView.wash[key])) wash[key] = cssFor(`wash.${key}`, lightTokens, errors);
820
- }
821
- }
822
-
823
- // Ramp steps as @theme text-utility data: size/line-height as literals; weight and tracking as
824
- // var() references when they alias a personality primitive, so the utilities track the cascade.
825
- const ramp = {};
826
- for (const step of RAMP_STEPS) {
827
- const t = lightView.typography?.ramp?.[step];
828
- if (!isToken(t) || !t.$value || typeof t.$value !== "object") { errors.push(`emit — typography.ramp.${step}: missing composite`); continue; }
829
- const v = t.$value;
830
- const weightVar = { "{primitive.font-weight-strong}": "var(--font-weight-strong)", "{primitive.font-weight-normal}": "var(--font-weight-normal)" };
831
- let fontWeight;
832
- if (typeof v.fontWeight === "number") fontWeight = String(v.fontWeight);
833
- else if (weightVar[v.fontWeight]) fontWeight = weightVar[v.fontWeight];
834
- else {
835
- const target = resolveAlias(v.fontWeight, lightTokens, "fontWeight", `typography.ramp.${step}.fontWeight`, errors);
836
- fontWeight = target ? String(target.$value) : "400";
837
- }
838
- const preservedTracking = t.$extensions?.[EXT]?.css?.letterSpacing;
839
- let letterSpacing;
840
- if (v.letterSpacing === "{primitive.heading-tracking}") letterSpacing = "var(--heading-tracking)";
841
- else if (typeof preservedTracking === "string") letterSpacing = preservedTracking;
842
- else if (typeof v.letterSpacing === "number") letterSpacing = `${v.letterSpacing}em`;
843
- else {
844
- const target = resolveAlias(v.letterSpacing, lightTokens, "number", `typography.ramp.${step}.letterSpacing`, errors);
845
- letterSpacing = target ? `${target.$value}em` : "0em";
846
- }
847
- ramp[step] = {
848
- fontSize: `${v.fontSize?.value}${v.fontSize?.unit ?? "px"}`,
849
- lineHeight: String(v.lineHeight),
850
- fontWeight,
851
- letterSpacing,
852
- };
853
- }
854
-
855
- return { primitives, elevation, wash, ramp, manifest: { "color-tokens": colorTokens } };
856
- }
857
-
858
- // ---------------------------------------------------------------------------
859
- // Contract 3 — emitter completeness: every authored group reaches consumable output
860
- // ---------------------------------------------------------------------------
861
-
862
- // Refuse emission when any resolved view carries an authored token group (or typography subgroup)
863
- // absent from the emitter registry — machinery that accepts data it never emits is a silent breach.
864
- function checkEmitterCompleteness(views, emitErrors) {
865
- for (const [context, view] of views) {
866
- const label = context === "light" ? "" : ` [${context}]`;
867
- for (const group of Object.keys(view)) {
868
- if (group.startsWith("$")) continue;
869
- if (!EMITTED_GROUPS.has(group)) {
870
- emitErrors.push(`emitter completeness — authored token group "${group}"${label} has no emitter (contract 3): an authored group with no emitter simply vanishes; add its emitter to this script or remove the group. Emitters exist for: ${[...EMITTED_GROUPS.keys()].join(", ")}.`);
871
- }
872
- }
873
- for (const sub of Object.keys(view.typography ?? {})) {
874
- if (!sub.startsWith("$") && !TYPOGRAPHY_SUBGROUPS.includes(sub)) {
875
- emitErrors.push(`emitter completeness — authored token group "typography.${sub}"${label} has no emitter (contract 3): only typography.family and typography.ramp reach the emitted output.`);
876
- }
877
- }
878
- }
879
- }
880
-
881
- // ---------------------------------------------------------------------------
882
- // Contract 2 — font binding: fonts.css generation + the fail-closed cod-16 gate
883
- // ---------------------------------------------------------------------------
884
-
885
- // Parse a CSV text into row objects keyed by its header (RFC quoting: "" escapes a quote inside a quoted field).
886
- function parseCsv(text) {
887
- const records = [];
888
- let field = "", record = [], inQuotes = false;
889
- const endField = () => { record.push(field); field = ""; };
890
- const endRecord = () => { endField(); if (record.some((f) => f !== "")) records.push(record); record = []; };
891
- for (let i = 0; i < text.length; i++) {
892
- const c = text[i];
893
- if (inQuotes) {
894
- if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
895
- else if (c === '"') inQuotes = false;
896
- else field += c;
897
- } else if (c === '"') inQuotes = true;
898
- else if (c === ",") endField();
899
- else if (c === "\n") endRecord();
900
- else if (c !== "\r") field += c;
901
- }
902
- endRecord();
903
- const header = records.shift() ?? [];
904
- return records.map((cells) => Object.fromEntries(header.map((h, i) => [h, cells[i] ?? ""])));
905
- }
906
-
907
- // Load the family → registering-import resolver: typography.csv css-import rows first (paste-ready
908
- // static @fontsource imports, registering the plain family names), fonts-known.json as the fallback
909
- // oracle (family → static weights). Returns resolve(family) → { family, pkg, imports } | null.
910
- function loadFontResolver() {
911
- const byFamily = new Map();
912
- try {
913
- for (const row of parseCsv(readFileSync(join(COMBINATIONS_DIR, "typography.csv"), "utf8"))) {
914
- for (const col of ["heading_family", "body_family", "mono_family"]) {
915
- const family = (row[col] ?? "").trim();
916
- if (family === "" || byFamily.has(family.toLowerCase())) continue;
917
- const slug = familySlug(family);
918
- const imports = [...String(row["css-import"] ?? "").matchAll(/@import\s+'(@fontsource\/([a-z0-9-]+)\/[^']+)'/g)]
919
- .filter((m) => m[2] === slug)
920
- .map((m) => `@import '${m[1]}';`);
921
- if (imports.length > 0) byFamily.set(family.toLowerCase(), { family, pkg: `@fontsource/${slug}`, imports });
922
- }
923
- }
924
- } catch { /* typography.csv unreadable — fonts-known.json stays the oracle */ }
925
- let knownFamilies = {};
926
- try { knownFamilies = JSON.parse(readFileSync(join(COMBINATIONS_DIR, "fonts-known.json"), "utf8")).families ?? {}; } catch { /* absent oracle degrades to csv-only resolution */ }
927
- const knownLower = new Map(Object.entries(knownFamilies).map(([name, weights]) => [name.toLowerCase(), weights]));
928
- return (family) => {
929
- const hit = byFamily.get(family.toLowerCase());
930
- if (hit) return hit;
931
- const weights = knownLower.get(family.toLowerCase());
932
- if (!Array.isArray(weights) || weights.length === 0) return null;
933
- const slug = familySlug(family);
934
- return { family, pkg: `@fontsource/${slug}`, imports: weights.map((w) => `@import '@fontsource/${slug}/${w}.css';`) };
935
- };
936
- }
937
-
938
- // Collect the artifact's font surfaces: the two emitted stacks (the cod-16 gate's subjects) and the
939
- // deduped named families across every fontFamily token (import generation covers all of them).
940
- function collectFontSurfaces(views, doc, errors) {
941
- const lightTokens = collectTokens(views.get("light"));
942
- const stacks = [
943
- { name: "--font-sans", families: parseFontStackCss(String(doc.primitives["font-sans"])) },
944
- { name: "--font-mono", families: parseFontStackCss(String(doc.primitives["font-mono"])) },
945
- ];
946
- const importFamilies = [];
947
- const seen = new Set();
948
- const push = (family) => {
949
- const key = family.toLowerCase();
950
- if (!seen.has(key) && !SYSTEM_FONT_KEYWORDS.has(key)) { seen.add(key); importFamilies.push(family); }
951
- };
952
- for (const stack of stacks) for (const family of stack.families) push(family);
953
- for (const slot of ["heading", "body", "mono", "display"]) {
954
- if (!lightTokens.has(`typography.family.${slot}`)) continue;
955
- for (const family of parseFontStackCss(cssFor(`typography.family.${slot}`, lightTokens, errors))) push(family);
956
- }
957
- return { stacks, importFamilies };
958
- }
959
-
960
- // Read the emit target's dependency map, or null when the target is standalone (no package.json) —
961
- // the standalone case degrades the cod-16 gate to import-list validation.
962
- function readTargetDeps(outdir) {
963
- const pkgPath = join(outdir, "package.json");
964
- if (!existsSync(pkgPath)) return null;
965
- try {
966
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
967
- return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
968
- } catch {
969
- return null;
970
- }
971
- }
972
-
973
- // The emitted src/styles/fonts.css content: the generated registering-import list, one derivation.
974
- function emitFontsCss(entries, unresolved) {
975
- const lines = [
976
- "/* src/styles/fonts.css — @fontsource font registrations (emitted; do not hand-edit).",
977
- " *",
978
- " * One derivation, not two authored artifacts: this import list is GENERATED from the",
979
- " * artifact's fontFamily tokens (typography.csv css-import rows; fonts-known.json fallback),",
980
- " * and the emitted stacks use the same registered family names — so the flagship font cannot",
981
- " * silently fail to bind (cod-16 gates the pair at --emit). Loaded FIRST by globals.css",
982
- " * (step 1 of the five-step import order): faces register before any stack consumes them.",
983
- " * To change a family, edit design.tokens.json and re-run derive-personality.mjs --emit. */",
984
- ];
985
- for (const entry of entries) lines.push("", `/* ${entry.family} — ${entry.pkg} */`, ...entry.imports);
986
- for (const family of unresolved) lines.push("", `/* UNRESOLVED: "${family}" — no known @fontsource package registers it; register it via @font-face or swap the family (cod-16). */`);
987
- if (entries.length === 0 && unresolved.length === 0) lines.push("", "/* every stack family is a system/generic face — nothing to register. */");
988
- return lines.join("\n") + "\n";
989
- }
990
-
991
- // Contract 2 in full: generate fonts.css from the artifact's fontFamily tokens and run the cod-16
992
- // binding gate over the emitted stacks — (a) each named family rides a generated import whose
993
- // package is installed (project targets; standalone targets degrade to import-list validation), or
994
- // (b) is a recognized system/generic family. Returns { fontsCss, failures }.
995
- function buildFontBinding(views, doc, outdir, errors) {
996
- const resolveFamily = loadFontResolver();
997
- const { stacks, importFamilies } = collectFontSurfaces(views, doc, errors);
998
- const deps = readTargetDeps(outdir);
999
- const entries = [];
1000
- const unresolved = [];
1001
- for (const family of importFamilies) {
1002
- const hit = resolveFamily(family);
1003
- if (hit) { entries.push(hit); continue; }
1004
- const variablePkg = `@fontsource-variable/${familySlug(family)}`;
1005
- if (deps !== null && variablePkg in deps) {
1006
- // The consumer already installed the variable package (registers the "<Family> Variable" name).
1007
- entries.push({ family, pkg: variablePkg, imports: [`@import '${variablePkg}';`] });
1008
- continue;
1009
- }
1010
- unresolved.push(family);
1011
- }
1012
- const entryByFamily = new Map(entries.map((entry) => [entry.family.toLowerCase(), entry]));
1013
- const failures = [];
1014
- for (const stack of stacks) {
1015
- for (const family of stack.families) {
1016
- const key = family.toLowerCase();
1017
- if (SYSTEM_FONT_KEYWORDS.has(key)) continue;
1018
- const entry = entryByFamily.get(key);
1019
- const slug = familySlug(family);
1020
- if (!entry) {
1021
- failures.push(`"${family}" (emitted ${stack.name} stack): no known @fontsource package registers it (typography.csv, fonts-known.json${deps !== null ? ", package.json" : ""}) — install and register one (npm install @fontsource/${slug}, then re-run --emit) or swap to a bindable family / system generic.`);
1022
- } else if (deps !== null && !(entry.pkg in deps) && !(`@fontsource/${slug}` in deps) && !(`@fontsource-variable/${slug}` in deps)) {
1023
- failures.push(`"${family}" (emitted ${stack.name} stack): its registering import needs ${entry.pkg}, absent from package.json — npm install ${entry.pkg}`);
1024
- }
1025
- }
1026
- }
1027
- return { fontsCss: emitFontsCss(entries, unresolved), failures };
1028
- }
1029
-
1030
- // ---------------------------------------------------------------------------
1031
- // Emitters (values come from the token artifact; non-comment output is v1 byte-identical)
1032
- // ---------------------------------------------------------------------------
1033
-
1034
- // Derive a rosterless data-style slug from the product name.
1035
- function slugify(name) {
1036
- return String(name || "project").toLowerCase().trim()
1037
- .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
1038
- }
1039
-
1040
- // Quote multi-word font family names so the emitted CSS font stack is valid.
1041
- function quoteFontStack(stack) {
1042
- return String(stack).split(",").map((f) => {
1043
- const t = f.trim();
1044
- if (t === "") return t;
1045
- if (t.startsWith('"') || t.startsWith("'")) return t;
1046
- return /\s/.test(t) ? `"${t}"` : t;
1047
- }).filter((t) => t !== "").join(", ");
1048
- }
1049
-
1050
- // Derive --ease-enter: reuse ease-default if it is already a decelerate curve, else the emphasized-decelerate arrival curve.
1051
- function deriveEaseEnter(easeDefault) {
1052
- const m = /cubic-bezier\(\s*[-\d.]+\s*,\s*([-\d.]+)/.exec(String(easeDefault));
1053
- if (m && parseFloat(m[1]) >= 0.9) return easeDefault;
1054
- return "cubic-bezier(0.16, 1, 0.3, 1)";
1055
- }
1056
-
1057
- // Build the [data-style] primitive lines (governed density refs + derived temporals inserted after
1058
- // the temporal family), then the elevation composites and the optional wash pair.
1059
- function buildStyleBlockLines(doc) {
1060
- const primitives = doc.primitives;
1061
- const easeEnter = deriveEaseEnter(primitives["ease-default"]);
1062
- const lines = [];
1063
- for (const key of PRIMITIVES) {
1064
- if (key === "density-gap") lines.push(" --density-gap: var(--density-base-gap);");
1065
- else if (key === "density-inset") lines.push(" --density-inset: var(--density-base-inset);");
1066
- else if (key === "font-sans") lines.push(` --font-sans: ${quoteFontStack(primitives["font-sans"])};`);
1067
- else if (key === "font-mono") lines.push(` --font-mono: ${quoteFontStack(primitives["font-mono"])};`);
1068
- else lines.push(` --${key}: ${primitives[key]};`);
1069
- if (key === "stagger-unit") {
1070
- lines.push(` --ease-enter: ${easeEnter}; /* derived from ease-default */`);
1071
- lines.push(" --ease-exit: var(--ease-default); /* derived: departure mirrors the neutral curve */");
1072
- lines.push(" --stagger-delay: var(--stagger-unit); /* derived: the motion-stagger cascade base */");
1073
- }
1074
- }
1075
- lines.push(" /* elevation (4) — designed shadow composites; --shadow-intensity stays the master multiplier */");
1076
- for (const level of ELEVATION_TOKENS) lines.push(` --shadow-${level}: ${doc.elevation[level]};`);
1077
- if (doc.wash) {
1078
- lines.push(" /* wash (optional, <= 2) — low-chroma tint ramps, consumed only on budget-granted surfaces */");
1079
- for (const key of WASH_TOKENS) {
1080
- if (doc.wash[key] !== undefined) lines.push(` --wash-${key}: ${doc.wash[key]};`);
1081
- }
1082
- }
1083
- return lines.join("\n");
1084
- }
1085
-
1086
- // Emit the full src/styles/personality.css content for a passing artifact.
1087
- function emitPersonalityCss(doc, slug, meta) {
1088
- const p = doc.primitives;
1089
- const light = doc.manifest["color-tokens"].light;
1090
- const density = p["density-tier"];
1091
- const verbosity = p.verbosity;
1092
- const feedback = p.feedback;
1093
-
1094
- const dark = doc.manifest["color-tokens"].dark;
1095
-
1096
- const semanticLines = SEMANTIC_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
1097
- const substrateLines = SUBSTRATE_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
1098
- const styleLines = buildStyleBlockLines(doc);
1099
-
1100
- const darkSection = dark
1101
- ? `/* ── Dark theme — OPT-IN ONLY (this look ships a dark overlay). ──
1102
- * CASCADE CONTRACT 1b: :root[data-theme="dark"] (0,2,0) — a lone [data-theme="dark"]
1103
- * attribute selector (0,1,0) loses to the library's later bare :root block by source
1104
- * order. design.tokens.dark.json remaps all 36 color tokens; <html data-theme="dark">
1105
- * opts in (theme-default decides the boot attribute). */
1106
- :root[data-theme="dark"] {
1107
- /* semantic (19) */
1108
- ${SEMANTIC_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
1109
- /* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
1110
- ${SUBSTRATE_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
1111
- }`
1112
- : `/* ── Dark theme — OPT-IN ONLY. ──
1113
- * Emitted only when the artifact ships a design.tokens.dark.json dark overlay,
1114
- * as :root[data-theme="dark"] (0,2,0 — armored against the library's later :root).
1115
- * This block never appears for a light-only project.
1116
- :root[data-theme="dark"] {
1117
- --background: ...; --foreground: ...; ... all 36 tokens remapped ...
1118
- } */`;
1119
-
1120
- return `/* src/styles/personality.css — operator-UI personality tokens (emitted; do not hand-edit).
1121
- *
1122
- * Active personality — code is the cache. These values and the <html data-*>
1123
- * attributes in index.html are emitted from the design-tokens artifact. To change
1124
- * the look, edit design.tokens.json and re-run derive-personality.mjs --emit.
1125
- * style: ${slug} (<html data-style="${slug}">) — the project's own personality (rosterless)
1126
- * density: ${density} (<html data-density="${density}">)
1127
- * verbosity: ${verbosity} (<html data-verbosity="${verbosity}">)
1128
- * feedback: ${feedback} (<html data-feedback="${feedback}">)
1129
- * theme: ${meta.theme} (<html data-theme="${meta.theme}">) — the payload's theme-default decides the boot theme
1130
- * artifact: design.tokens.json (resolved via design.resolver.json) — provenance: ${meta.provenance}, sourced ${meta.sourced}
1131
- *
1132
- * Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
1133
- * this file resolves those names to values. Components never use dark: prefixes or literal colors.
1134
- */
1135
-
1136
- /* ── System baseline: the 27 primitives at neutral operator defaults. ──
1137
- * The safety net if <html> ever carries no data-style. The project's real
1138
- * values live in the single [data-style="${slug}"] block below, which
1139
- * overrides these. Improve/extract rewrites that block, never this baseline. */
1140
- :root {
1141
- /* geometric */
1142
- --radius: 6px;
1143
- --shadow-intensity: 1;
1144
- --border-weight: 1px;
1145
- --density-base-gap: 1; /* the density tier writes this; see the density mechanism */
1146
- --density-base-inset: 1;
1147
- --density-gap: var(--density-base-gap);
1148
- --density-inset: var(--density-base-inset);
1149
- /* typographic — generics only: the baseline never names a webfont it cannot
1150
- * guarantee is registered (cod-16); the [data-style] block carries the real stacks. */
1151
- --font-sans: system-ui, sans-serif;
1152
- --font-mono: ui-monospace, SFMono-Regular, monospace;
1153
- --font-weight-normal: 400;
1154
- --font-weight-strong: 600;
1155
- --heading-tracking: -0.01em;
1156
- /* temporal (+ derived) */
1157
- --duration-unit: 200ms;
1158
- --duration-fast: calc(var(--duration-unit) * 0.5);
1159
- --duration-normal: var(--duration-unit);
1160
- --duration-slow: calc(var(--duration-unit) * 2);
1161
- --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
1162
- --stagger-unit: 30ms;
1163
- --ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
1164
- --ease-exit: var(--ease-default);
1165
- --stagger-delay: var(--stagger-unit);
1166
- /* surface */
1167
- --contrast-boost: 1;
1168
- /* elevation (neutral slate; the [data-style] block below carries the designed composites) */
1169
- --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)));
1170
- --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)));
1171
- --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)));
1172
- --shadow-overlay: 0 20px 25px -5px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 8px 10px -6px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
1173
- /* iconography */
1174
- --icon-stroke: 1.75;
1175
- --icon-size-base: 16px;
1176
- /* operator layout */
1177
- --sidebar-width-expanded: 240px;
1178
- --sidebar-width-collapsed: 56px;
1179
- --topbar-height: 52px;
1180
- --side-panel-width: 480px;
1181
- --side-panel-width-rich: 640px;
1182
- --modal-width-max: 480px;
1183
- --form-max-width: 720px;
1184
- --table-row-height: 40px;
1185
- /* behavioral fallbacks (match verbosity=default + feedback=subtle) */
1186
- --label-display: inline-flex;
1187
- --helper-text-display: block;
1188
- --tooltip-delay: 500ms;
1189
- --feedback-press-scale: 0.99;
1190
- --focus-ring-width: 2px;
1191
- --focus-ring-offset: 1px;
1192
- }
1193
-
1194
- /* ── Semantic + substrate color tokens — light theme. ──
1195
- * CASCADE CONTRACT 1: both theme blocks carry the same 0,2,0 armor — light as
1196
- * :root, :root[data-theme="light"] and dark as :root[data-theme="dark"] — because
1197
- * a bare :root or a lone attribute selector loses to the library's later :root
1198
- * block in @nurix/components/styles.css by source order. All 36 tokens are set
1199
- * (D6): no consumed token falls back to a shadcn-gray default. */
1200
- :root,
1201
- :root[data-theme="light"] {
1202
- /* semantic (19) */
1203
- ${semanticLines}
1204
- /* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
1205
- ${substrateLines}
1206
- }
1207
-
1208
- /* ── The project personality: the single [data-style] block (D3, rosterless). ──
1209
- * Named after the project — not a preset. This materializes the artifact's
1210
- * primitive token group (all 27 primitives + the derived temporals), the four
1211
- * elevation composites, and the optional wash pair. It is the one block
1212
- * improve/extract rewrites. density-gap/inset are governed refs, so the global
1213
- * tier and per-surface step still compose. */
1214
- [data-style="${slug}"] {
1215
- ${styleLines}
1216
- }
1217
-
1218
- /* ── Behavioral dials (D4) — orthogonal to the visual primitives. ── */
1219
- [data-verbosity="full"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 800ms; }
1220
- [data-verbosity="default"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 500ms; }
1221
- [data-verbosity="compact"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 200ms; }
1222
- [data-verbosity="expert"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 0ms; }
1223
-
1224
- [data-feedback="subtle"] { --feedback-press-scale: 0.99; --focus-ring-width: 2px; --focus-ring-offset: 1px; }
1225
- [data-feedback="minimal"] { --feedback-press-scale: 1; --focus-ring-width: 1px; --focus-ring-offset: 0px; }
1226
-
1227
- /* ── Density: global tier → base multipliers (D31). ──
1228
- * The tier writes ONLY the base; --density-gap/inset resolve from it (above),
1229
- * so a per-surface step can re-derive relative to the tier. */
1230
- [data-density="dense"] { --density-base-gap: 0.85; --density-base-inset: 0.85; }
1231
- [data-density="standard"] { --density-base-gap: 1; --density-base-inset: 1; }
1232
- [data-density="spacious"] { --density-base-gap: 1.15; --density-base-inset: 1.15; }
1233
-
1234
- /* ── Per-archetype density modifier (D31) — relative step on the tier, pure cascade, no JS. ──
1235
- * A surface element carries data-density-step; it re-derives one step off the
1236
- * inherited base. Reads --density-base-* (never itself) so it composes with the
1237
- * tier and never absolutely overrides it. The recomputed --density-gap flows into
1238
- * the library's p-* and gap-* through the @theme --spacing remap. */
1239
- [data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
1240
- [data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
1241
-
1242
- ${darkSection}
1243
-
1244
- /* ── Motion moments — named classes consuming the temporal tokens. ──
1245
- * motion-lift: hover lift (-1px + elevation.sm) at duration-fast/ease-default.
1246
- * motion-stagger: children arrival cascade at --stagger-delay, capped at 8
1247
- * children (mot-08); consumption is budget-gated by the surface stamp. Both are
1248
- * inert under prefers-reduced-motion (mot-03). */
1249
- @media (prefers-reduced-motion: no-preference) {
1250
- .motion-lift {
1251
- transition: transform var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default);
1252
- }
1253
- .motion-lift:hover {
1254
- transform: translateY(-1px);
1255
- box-shadow: var(--shadow-sm);
1256
- }
1257
- @keyframes motion-stagger-arrive {
1258
- from { opacity: 0; transform: translateY(4px); }
1259
- to { opacity: 1; transform: translateY(0); }
1260
- }
1261
- .motion-stagger > * {
1262
- animation: motion-stagger-arrive var(--duration-normal) var(--ease-enter) backwards;
1263
- animation-delay: calc(var(--stagger-delay) * 7); /* children past the 8-cap arrive with the 8th */
1264
- }
1265
- ${Array.from({ length: 8 }, (_, i) => ` .motion-stagger > :nth-child(${i + 1}) { animation-delay: calc(var(--stagger-delay) * ${i}); }`).join("\n")}
1266
- }
1267
-
1268
- /* ── Accessibility ── */
1269
- @media (prefers-reduced-motion: reduce) {
1270
- :root, [data-style] {
1271
- --duration-unit: 0ms; --stagger-unit: 0ms; --stagger-delay: 0ms;
1272
- --ease-default: linear; --ease-enter: linear; --ease-exit: linear;
1273
- --feedback-press-scale: 1;
1274
- }
1275
- }
1276
- @media (prefers-contrast: more) {
1277
- :root, [data-style] {
1278
- --contrast-boost: 1.15; --border-weight: 2px; --shadow-intensity: 0; --icon-stroke: 2;
1279
- }
1280
- }
1281
- `;
1282
- }
1283
-
1284
- // Emit the full src/styles/globals.css content (Tailwind v4 glue + @theme inline mappings).
1285
- function emitGlobalsCss(doc) {
1286
- const p = doc.primitives;
1287
- const fontSans = quoteFontStack(p["font-sans"]);
1288
- const fontMono = quoteFontStack(p["font-mono"]);
1289
- const fontWeightNormal = p["font-weight-normal"];
1290
- const colorMaps = ALL_TOKENS.map((t) => ` --color-${t}: var(--${t});`).join("\n");
1291
-
1292
- return `/* src/styles/globals.css — Tailwind v4 glue + @theme inline mappings (emitted). */
1293
-
1294
- /* CASCADE CONTRACT 2: this five-step order is LOAD-BEARING. fonts.css registers the
1295
- * @fontsource faces before any stack consumes them; the library's styles.css ships
1296
- * its own :root token defaults and its own @theme radius scale, so our @theme MUST
1297
- * come AFTER the import to win the radius scale and own every mapping.
1298
- * rules/frontend.md states the same order — co-authority. */
1299
- @import "./fonts.css"; /* 1. font registration (faces before stacks) */
1300
- @import "./personality.css"; /* 2. token source */
1301
- @import "tailwindcss"; /* 3. framework */
1302
- @import "@nurix/components/styles.css"; /* 4. library (before our @theme) */
1303
-
1304
- @theme inline { /* 5. consumer theme — AFTER the library, so it wins */
1305
- /* radius scale — shadcn offsets on --radius, re-asserted so the CONSUMER owns it.
1306
- * A flat personality sets --radius: 0 → every step collapses to 0. */
1307
- --radius-sm: calc(var(--radius) - 4px);
1308
- --radius-md: calc(var(--radius) - 2px);
1309
- --radius-lg: var(--radius);
1310
- --radius-xl: calc(var(--radius) + 4px);
1311
-
1312
- /* density remap — the single line that scales ALL library spacing. Every p-* and
1313
- * gap-* is calc(var(--spacing) x N); driving --spacing off --density-gap flows the
1314
- * tier AND the per-surface density step into the library's internals. */
1315
- --spacing: calc(0.25rem * var(--density-gap));
1316
-
1317
- /* shadow — the elevation.* composites become the shadow-* utilities; every alpha rides
1318
- * --shadow-intensity (2xs/xs stay the neutral slate hairlines the library leans on) */
1319
- --shadow-2xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.04 * var(--shadow-intensity)));
1320
- --shadow-xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.06 * var(--shadow-intensity)));
1321
- --shadow-sm: ${doc.elevation.sm};
1322
- --shadow-md: ${doc.elevation.md};
1323
- --shadow-lg: ${doc.elevation.lg};
1324
- --shadow-overlay: ${doc.elevation.overlay};
1325
-
1326
- /* font weight — two-weight discipline: medium/semibold/bold all resolve to the
1327
- * single emphasis weight; operator UI carries no third weight. normal is emitted
1328
- * as a literal because its theme key and the primitive share a name (no self-ref). */
1329
- --font-weight-normal: ${fontWeightNormal};
1330
- --font-weight-medium: var(--font-weight-strong);
1331
- --font-weight-semibold: var(--font-weight-strong);
1332
- --font-weight-bold: var(--font-weight-strong);
1333
-
1334
- /* tracking — heading tightness reaches the library's tracking-* utilities */
1335
- --tracking-tighter: var(--heading-tracking);
1336
- --tracking-tight: var(--heading-tracking);
1337
-
1338
- /* type ramp — the seven authored steps as text-* utilities (typ-09: the type scale IS the
1339
- * declared ramp; text-display is the scale-drama step, brand-forward surfaces only, typ-20) */
1340
- ${RAMP_STEPS.map((step) => {
1341
- const r = doc.ramp[step];
1342
- return ` --text-${step}: ${r.fontSize};
1343
- --text-${step}--line-height: ${r.lineHeight};
1344
- --text-${step}--font-weight: ${r.fontWeight};
1345
- --text-${step}--letter-spacing: ${r.letterSpacing};`;
1346
- }).join("\n")}
1347
-
1348
- /* fonts — emitted as the literal stack: a font is not a runtime-toggled token, and
1349
- * the theme key shares a name with the personality primitive (no self-reference).
1350
- * personality.css carries the same --font-sans/--font-mono for direct var() use. */
1351
- --font-sans: ${fontSans};
1352
- --font-mono: ${fontMono};
1353
-
1354
- /* transitions */
1355
- --transition-fast: var(--duration-fast) var(--ease-default);
1356
- --transition-normal: var(--duration-normal) var(--ease-default);
1357
- --transition-slow: var(--duration-slow) var(--ease-default);
1358
- --transition-enter: var(--duration-normal) var(--ease-enter);
1359
- --transition-exit: var(--duration-fast) var(--ease-exit);
1360
-
1361
- /* semantic + substrate color maps — all 36, so bg-popover / bg-chart-1 / bg-sidebar and the
1362
- * dark ladder's bg-surface-raised / bg-surface-overlay resolve.
1363
- * --color-destructive-foreground is mapped explicitly (D6): the library leaves it unmapped. */
1364
- ${colorMaps}
1365
- }
1366
-
1367
- /* dark variant — ALWAYS ACTIVE: redefines the library's class-based dark: variant to
1368
- * follow the data-theme attribute, so library dark: utilities (legal inside the library,
1369
- * banned in project JSX by the no-literals rule) track the same attribute the theme
1370
- * blocks armor. Inert until <html data-theme="dark">; harmless for a light-only project. */
1371
- @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
1372
- `;
1373
- }
1374
-
1375
- // Emit the index.html <html> attribute snippet the shell must carry.
1376
- function emitIndexHtmlAttributes(doc, slug, meta) {
1377
- const p = doc.primitives;
1378
- return `<!-- Personality state lives in these <html data-*> attributes — code is the cache.
1379
- The design-tokens artifact is the source. To change the look, edit design.tokens.json
1380
- and re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
1381
- <html
1382
- lang="en"
1383
- data-style="${slug}"
1384
- data-density="${p["density-tier"]}"
1385
- data-verbosity="${p.verbosity}"
1386
- data-feedback="${p.feedback}"
1387
- data-theme="${meta.theme}"
1388
- >
1389
- `;
1390
- }
1391
-
1392
- // Write a file, creating parent directories as needed.
1393
- function writeFileEnsured(path, content) {
1394
- mkdirSync(dirname(path), { recursive: true });
1395
- writeFileSync(path, content);
1396
- }
1397
-
1398
- // ---------------------------------------------------------------------------
1399
- // CLI
1400
- // ---------------------------------------------------------------------------
1401
-
1402
- // Print usage and exit non-zero.
1403
- function usage() {
1404
- process.stderr.write("usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir> [--provisional]] [--theme light|dark]\n");
1405
- process.exit(2);
1406
- }
1407
-
1408
- // Entry point: parse argv, run --check or --emit over the artifact directory.
1409
- function main() {
1410
- const argv = process.argv.slice(2);
1411
- let artifactDir = null;
1412
- let mode = "check";
1413
- let outdir = null;
1414
- let theme = null; // null = follow the payload's theme-default
1415
- let provisional = false;
1416
- for (let i = 0; i < argv.length; i++) {
1417
- const a = argv[i];
1418
- if (a === "--check") mode = "check";
1419
- else if (a === "--emit") { mode = "emit"; outdir = argv[++i] ?? null; }
1420
- else if (a === "--theme") { theme = argv[++i] ?? null; }
1421
- else if (a === "--provisional") provisional = true;
1422
- else if (!a.startsWith("-") && artifactDir === null) artifactDir = a;
1423
- else usage();
1424
- }
1425
- if (!artifactDir) usage();
1426
- if (mode === "emit" && !outdir) usage();
1427
- if (provisional && mode !== "emit") usage();
1428
- if (theme !== null && !LEGAL_THEMES.includes(theme)) usage();
1429
-
1430
- const { errors, payload, views, hints, theme: bootTheme, hasDark, hasWash } = runCheck(artifactDir, theme);
1431
-
1432
- if (errors.length > 0) {
1433
- process.stderr.write(`check FAILED (${artifactDir}): ${errors.length} issue(s)\n`);
1434
- for (const e of errors) process.stderr.write(` - ${e}\n`);
1435
- process.exit(1);
1436
- }
1437
-
1438
- const slug = slugify(payload.product);
1439
-
1440
- if (mode === "check") {
1441
- const hintNote = hints.length > 0 ? `, ${hints.length} hint(s) outstanding (emit will refuse)` : "";
1442
- process.stdout.write(`check PASSED (${artifactDir}): rules 1–15 OK — 27 primitives, 36 color roles, typography 3+7, elevation 4${hasWash ? ", wash present" : ""}, payload/manifest/parse-back OK — provenance=${payload.provenance}, style=${slug}, theme=${bootTheme}${hasDark ? ", dark overlay present" : ""}${hintNote}\n`);
1443
- process.exit(0);
1444
- }
1445
-
1446
- // Rule 12 — emission precondition: refuse while any token carries a hint. --provisional (the
1447
- // golden/regression flow) emits hinted tokens at their provisional $values instead — loudly.
1448
- if (hints.length > 0 && !provisional) {
1449
- process.stderr.write(`emit REFUSED (${artifactDir}): ${hints.length} unresolved hint(s) — rule 12: every gated token must be concrete before emission.\n`);
1450
- for (const h of hints) process.stderr.write(` - ${h.key}: hint: ${h.text}\n`);
1451
- process.stderr.write(" Resolve each with judgment: set the real $value, delete the hint key, record the rationale in $description — then re-run --emit.\n");
1452
- process.exit(1);
1453
- }
1454
- if (hints.length > 0) {
1455
- process.stdout.write(`emit PROVISIONAL (${artifactDir}): ${hints.length} hint(s) emitted at their provisional $values — regression/golden flow only, never shipped output.\n`);
1456
- }
1457
-
1458
- const emitErrors = [];
1459
- const doc = buildEmitterDoc(views, payload, emitErrors);
1460
- checkEmitterCompleteness(views, emitErrors);
1461
- const fontBinding = buildFontBinding(views, doc, outdir, emitErrors);
1462
- if (emitErrors.length > 0) {
1463
- process.stderr.write(`emit FAILED (${artifactDir}): ${emitErrors.length} issue(s)\n`);
1464
- for (const e of emitErrors) process.stderr.write(` - ${e}\n`);
1465
- process.exit(1);
1466
- }
1467
-
1468
- // Contract 2 — the cod-16 font-binding gate, fail-closed at --emit. --provisional (the golden/
1469
- // regression flow over hint-carrying or brand-font library twins) demotes it to a loud warning.
1470
- if (fontBinding.failures.length > 0) {
1471
- if (!provisional) {
1472
- process.stderr.write(`emit FAILED (${artifactDir}): font binding (cod-16) — ${fontBinding.failures.length} unbound famil${fontBinding.failures.length === 1 ? "y" : "ies"}\n`);
1473
- for (const f of fontBinding.failures) process.stderr.write(` - ${f}\n`);
1474
- process.exit(1);
1475
- }
1476
- process.stdout.write(`emit PROVISIONAL font-binding warning (${artifactDir}): ${fontBinding.failures.length} unbound famil${fontBinding.failures.length === 1 ? "y" : "ies"} — cod-16 fails closed at a real --emit\n`);
1477
- for (const f of fontBinding.failures) process.stdout.write(` - ${f}\n`);
1478
- }
1479
-
1480
- const meta = { provenance: payload.provenance, sourced: payload.sourced, theme: bootTheme };
1481
- const personalityCss = emitPersonalityCss(doc, slug, meta);
1482
- const globalsCss = emitGlobalsCss(doc);
1483
- const htmlAttrs = emitIndexHtmlAttributes(doc, slug, meta);
1484
- const personalityPath = join(outdir, "src/styles/personality.css");
1485
- const globalsPath = join(outdir, "src/styles/globals.css");
1486
- const fontsPath = join(outdir, "src/styles/fonts.css");
1487
- const attrsPath = join(outdir, "index-html-attributes.txt");
1488
- writeFileEnsured(personalityPath, personalityCss);
1489
- writeFileEnsured(globalsPath, globalsCss);
1490
- writeFileEnsured(fontsPath, fontBinding.fontsCss);
1491
- writeFileEnsured(attrsPath, htmlAttrs);
1492
-
1493
- process.stdout.write(`emitted personality system for style=${slug} → ${outdir}\n`);
1494
- process.stdout.write(` ${personalityPath}\n ${globalsPath}\n ${fontsPath}\n ${attrsPath}\n`);
1495
- process.exit(0);
1496
- }
1497
-
1498
- main();