@nurix/etna 0.19.11 → 0.19.13

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 (494) hide show
  1. package/README.md +6 -4
  2. package/data/harness-index.json +1 -1
  3. package/data/harnesses/code/agents/maintenance.md +1 -1
  4. package/data/harnesses/code/harness.json +1 -1
  5. package/data/harnesses/code/rules/frontend.md +5 -5
  6. package/data/harnesses/code/skills/design/SKILL.md +70 -50
  7. package/data/harnesses/code/skills/design/components/charts.md +76 -0
  8. package/data/harnesses/code/skills/design/components/collections.md +132 -0
  9. package/data/harnesses/code/skills/design/components/feedback.md +47 -0
  10. package/data/harnesses/code/skills/design/components/hooks.md +35 -0
  11. package/data/harnesses/code/skills/design/components/index.md +143 -0
  12. package/data/harnesses/code/skills/design/components/inputs.md +327 -0
  13. package/data/harnesses/code/skills/design/components/layout.md +52 -0
  14. package/data/harnesses/code/skills/design/components/media.md +66 -0
  15. package/data/harnesses/code/skills/design/components/motion.md +82 -0
  16. package/data/harnesses/code/skills/design/components/navigation.md +29 -0
  17. package/data/harnesses/code/skills/design/components/overlays.md +72 -0
  18. package/data/harnesses/code/skills/design/components/setup.md +40 -0
  19. package/data/harnesses/code/skills/design/components/typography.md +46 -0
  20. package/data/harnesses/code/skills/design/components/utils.md +37 -0
  21. package/data/harnesses/code/skills/design/data/blueprints/browse.md +38 -0
  22. package/data/harnesses/code/skills/design/data/blueprints/creation-flow.md +38 -0
  23. package/data/harnesses/code/skills/design/data/blueprints/dashboard.md +38 -0
  24. package/data/harnesses/code/skills/design/data/blueprints/grid.md +38 -0
  25. package/data/harnesses/code/skills/design/data/blueprints/queue.md +38 -0
  26. package/data/harnesses/code/skills/design/data/blueprints/record-detail.md +38 -0
  27. package/data/harnesses/code/skills/design/data/blueprints/settings.md +38 -0
  28. package/data/harnesses/code/skills/design/data/chart-styling/Readme.md +33 -0
  29. package/data/harnesses/code/skills/design/data/chart-styling/carbon-accessible-alert-semantic.md +19 -0
  30. package/data/harnesses/code/skills/design/data/chart-styling/carbon-monochrome-sequential-trend.md +19 -0
  31. package/data/harnesses/code/skills/design/data/chart-styling/datadog-classic-six-aligned.md +19 -0
  32. package/data/harnesses/code/skills/design/data/chart-styling/datawrapper-tiered-gray-hierarchy.md +19 -0
  33. package/data/harnesses/code/skills/design/data/chart-styling/econ-red-rule-horizontal.md +19 -0
  34. package/data/harnesses/code/skills/design/data/chart-styling/financial-diverging-threshold-overlay.md +19 -0
  35. package/data/harnesses/code/skills/design/data/chart-styling/ft-vocabulary-ranked-muted.md +19 -0
  36. package/data/harnesses/code/skills/design/data/chart-styling/grafana-dark-threshold-bands.md +19 -0
  37. package/data/harnesses/code/skills/design/data/chart-styling/material-rounded-explicit-label.md +19 -0
  38. package/data/harnesses/code/skills/design/data/chart-styling/nyt-ghost-grid-direct-label.md +19 -0
  39. package/data/harnesses/code/skills/design/data/chart-styling/plot-currentcolor-inherit.md +19 -0
  40. package/data/harnesses/code/skills/design/data/chart-styling/saas-gradient-glow-area.md +19 -0
  41. package/data/harnesses/code/skills/design/data/chart-styling/shadcn-composable-css-vars.md +19 -0
  42. package/data/harnesses/code/skills/design/data/charts/census.csv +58 -0
  43. package/data/harnesses/code/skills/design/data/checklists/browse.md +21 -0
  44. package/data/harnesses/code/skills/design/data/checklists/creation-flow.md +20 -0
  45. package/data/harnesses/code/skills/design/data/checklists/dashboard.md +21 -0
  46. package/data/harnesses/code/skills/design/data/checklists/grid.md +21 -0
  47. package/data/harnesses/code/skills/design/data/checklists/queue.md +21 -0
  48. package/data/harnesses/code/skills/design/data/checklists/record-detail.md +21 -0
  49. package/data/harnesses/code/skills/design/data/checklists/settings.md +21 -0
  50. package/data/harnesses/code/skills/design/data/chrome/board-column.md +68 -0
  51. package/data/harnesses/code/skills/design/data/chrome/data-table.md +63 -0
  52. package/data/harnesses/code/skills/design/data/chrome/side-panel.md +60 -0
  53. package/data/harnesses/code/skills/design/data/chrome/sidebar.md +78 -0
  54. package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +62 -0
  55. package/data/harnesses/code/skills/design/data/chrome/topbar.md +65 -0
  56. package/data/harnesses/code/skills/design/data/color/accent-presets.md +90 -0
  57. package/data/harnesses/code/skills/design/data/color/aesop-earth-mute.md +91 -0
  58. package/data/harnesses/code/skills/design/data/color/arc-peach-play.md +90 -0
  59. package/data/harnesses/code/skills/design/data/color/attio-hunter-teal.md +90 -0
  60. package/data/harnesses/code/skills/design/data/color/carbon-cyan-signal.md +90 -0
  61. package/data/harnesses/code/skills/design/data/color/clay-parchment.md +90 -0
  62. package/data/harnesses/code/skills/design/data/color/duolingo-feather.md +90 -0
  63. package/data/harnesses/code/skills/design/data/color/geist-mono-signal.md +90 -0
  64. package/data/harnesses/code/skills/design/data/color/linear-graphite-violet.md +90 -0
  65. package/data/harnesses/code/skills/design/data/color/mercury-onyx-cobalt.md +90 -0
  66. package/data/harnesses/code/skills/design/data/color/notion-paper-ink.md +90 -0
  67. package/data/harnesses/code/skills/design/data/color/radix-slate-iris.md +90 -0
  68. package/data/harnesses/code/skills/design/data/color/raycast-void-spectrum.md +90 -0
  69. package/data/harnesses/code/skills/design/data/color/spectrum-celery-fuchsia.md +90 -0
  70. package/data/harnesses/code/skills/design/data/color/stripe-blurple-ruby.md +90 -0
  71. package/data/harnesses/code/skills/design/data/color/superhuman-cod-picton.md +89 -0
  72. package/data/harnesses/code/skills/design/data/corpora/operations-review/companies.csv +15 -0
  73. package/data/harnesses/code/skills/design/data/corpora/operations-review/corpus.md +87 -0
  74. package/data/harnesses/code/skills/design/data/corpora/operations-review/people.csv +25 -0
  75. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/companies.csv +15 -0
  76. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/corpus.md +83 -0
  77. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/people.csv +25 -0
  78. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/companies.csv +15 -0
  79. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/corpus.md +89 -0
  80. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/people.csv +25 -0
  81. package/data/harnesses/code/skills/design/data/elevation/ant-design-directional-shadow.md +19 -0
  82. package/data/harnesses/code/skills/design/data/elevation/apple-liquid-glass-material.md +18 -0
  83. package/data/harnesses/code/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +20 -0
  84. package/data/harnesses/code/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +20 -0
  85. package/data/harnesses/code/skills/design/data/elevation/github-primer-sparing-shadow.md +19 -0
  86. package/data/harnesses/code/skills/design/data/elevation/ibm-carbon-layer-and-border.md +19 -0
  87. package/data/harnesses/code/skills/design/data/elevation/linear-dark-surface-ladder.md +17 -0
  88. package/data/harnesses/code/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +19 -0
  89. package/data/harnesses/code/skills/design/data/elevation/material-3-tonal-elevation.md +21 -0
  90. package/data/harnesses/code/skills/design/data/elevation/neobrutalist-hard-offset.md +19 -0
  91. package/data/harnesses/code/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +18 -0
  92. package/data/harnesses/code/skills/design/data/elevation/notion-warm-minimal-hairline.md +17 -0
  93. package/data/harnesses/code/skills/design/data/elevation/polaris-elevation-inset-bevel.md +21 -0
  94. package/data/harnesses/code/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +19 -0
  95. package/data/harnesses/code/skills/design/data/elevation/shadcn-neutral-derived-radius.md +19 -0
  96. package/data/harnesses/code/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +18 -0
  97. package/data/harnesses/code/skills/design/data/elevation/tailwind-default-utility-scale.md +19 -0
  98. package/data/harnesses/code/skills/design/data/elevation/vercel-geist-shadow-as-border.md +20 -0
  99. package/data/harnesses/code/skills/design/data/gradients/amber-attention-fill.md +16 -0
  100. package/data/harnesses/code/skills/design/data/gradients/cloud-shadow-wash.md +16 -0
  101. package/data/harnesses/code/skills/design/data/gradients/confident-lavender-wash.md +16 -0
  102. package/data/harnesses/code/skills/design/data/gradients/ember-ship-flare.md +18 -0
  103. package/data/harnesses/code/skills/design/data/gradients/full-metal-wash.md +16 -0
  104. package/data/harnesses/code/skills/design/data/gradients/indigo-ruby-cta-band.md +18 -0
  105. package/data/harnesses/code/skills/design/data/gradients/linear-graphite-wash.md +18 -0
  106. package/data/harnesses/code/skills/design/data/gradients/marble-quartz-wash.md +16 -0
  107. package/data/harnesses/code/skills/design/data/gradients/midnight-aurora-panel.md +16 -0
  108. package/data/harnesses/code/skills/design/data/gradients/neutral-volume-fill.md +16 -0
  109. package/data/harnesses/code/skills/design/data/gradients/revenue-uptrend-fill.md +16 -0
  110. package/data/harnesses/code/skills/design/data/gradients/risk-decline-fill.md +16 -0
  111. package/data/harnesses/code/skills/design/data/gradients/signal-blue-develop.md +18 -0
  112. package/data/harnesses/code/skills/design/data/gradients/slate-mist-wash.md +16 -0
  113. package/data/harnesses/code/skills/design/data/gradients/stripe-atmosphere-mesh.md +18 -0
  114. package/data/harnesses/code/skills/design/data/gradients/teal-signal-accent.md +16 -0
  115. package/data/harnesses/code/skills/design/data/gradients/violet-magenta-promo.md +16 -0
  116. package/data/harnesses/code/skills/design/data/iconography.md +199 -0
  117. package/data/harnesses/code/skills/design/data/motion/accordion-expand.md +18 -0
  118. package/data/harnesses/code/skills/design/data/motion/destructive-hold-confirm.md +18 -0
  119. package/data/harnesses/code/skills/design/data/motion/drag-dismiss-momentum.md +18 -0
  120. package/data/harnesses/code/skills/design/data/motion/empty-state-arrive.md +18 -0
  121. package/data/harnesses/code/skills/design/data/motion/hover-lift.md +18 -0
  122. package/data/harnesses/code/skills/design/data/motion/modal-center-arrive.md +18 -0
  123. package/data/harnesses/code/skills/design/data/motion/number-ticker-roll.md +18 -0
  124. package/data/harnesses/code/skills/design/data/motion/onboarding-bounce.md +18 -0
  125. package/data/harnesses/code/skills/design/data/motion/panel-slide.md +18 -0
  126. package/data/harnesses/code/skills/design/data/motion/press-feedback.md +18 -0
  127. package/data/harnesses/code/skills/design/data/motion/progress-linear-loop.md +18 -0
  128. package/data/harnesses/code/skills/design/data/motion/route-crossfade.md +18 -0
  129. package/data/harnesses/code/skills/design/data/motion/rubber-band-overscroll.md +18 -0
  130. package/data/harnesses/code/skills/design/data/motion/skeleton-shimmer.md +18 -0
  131. package/data/harnesses/code/skills/design/data/motion/stagger-arrive.md +18 -0
  132. package/data/harnesses/code/skills/design/data/motion/toast-enter-stack.md +18 -0
  133. package/data/harnesses/code/skills/design/data/motion/tooltip-chain-instant.md +18 -0
  134. package/data/harnesses/code/skills/design/data/motion/trigger-anchored-reveal.md +18 -0
  135. package/data/harnesses/code/skills/design/data/tells.csv +34 -0
  136. package/data/harnesses/code/skills/design/data/typography/anton-work-sans.md +17 -0
  137. package/data/harnesses/code/skills/design/data/typography/archivo-black-archivo.md +17 -0
  138. package/data/harnesses/code/skills/design/data/typography/bitter-karla.md +17 -0
  139. package/data/harnesses/code/skills/design/data/typography/dm-serif-dm-sans.md +17 -0
  140. package/data/harnesses/code/skills/design/data/typography/fraunces-inter.md +17 -0
  141. package/data/harnesses/code/skills/design/data/typography/geist-geist-mono.md +17 -0
  142. package/data/harnesses/code/skills/design/data/typography/hubot-sans-mona-sans.md +17 -0
  143. package/data/harnesses/code/skills/design/data/typography/ibm-plex-sans-operator-single-family.md +17 -0
  144. package/data/harnesses/code/skills/design/data/typography/instrument-serif-instrument-sans.md +17 -0
  145. package/data/harnesses/code/skills/design/data/typography/inter-operator-single-family.md +17 -0
  146. package/data/harnesses/code/skills/design/data/typography/libre-baskerville-libre-franklin.md +17 -0
  147. package/data/harnesses/code/skills/design/data/typography/newsreader-inter.md +17 -0
  148. package/data/harnesses/code/skills/design/data/typography/playfair-display-source-sans.md +17 -0
  149. package/data/harnesses/code/skills/design/data/typography/public-sans-operator-single-family.md +17 -0
  150. package/data/harnesses/code/skills/design/data/typography/sohne-inter-fintech.md +17 -0
  151. package/data/harnesses/code/skills/design/data/typography/source-serif-source-sans.md +17 -0
  152. package/data/harnesses/code/skills/design/data/typography/space-grotesk-work-sans-space-mono.md +17 -0
  153. package/data/harnesses/code/skills/design/data/typography/spectral-manrope.md +17 -0
  154. package/data/harnesses/code/skills/design/data/voices/bright-collaborator.md +29 -0
  155. package/data/harnesses/code/skills/design/data/voices/direct-operator.md +29 -0
  156. package/data/harnesses/code/skills/design/data/voices/earnest-helper.md +29 -0
  157. package/data/harnesses/code/skills/design/data/voices/editorial-composed.md +29 -0
  158. package/data/harnesses/code/skills/design/data/voices/quiet-concierge.md +29 -0
  159. package/data/harnesses/code/skills/design/data/voices/warm-guide.md +29 -0
  160. package/data/harnesses/code/skills/design/design-md/SCHEMA.md +179 -0
  161. package/data/harnesses/code/skills/design/design-md/index.csv +67 -0
  162. package/data/harnesses/code/skills/design/design-md/library/airbnb.md +207 -0
  163. package/data/harnesses/code/skills/design/design-md/library/airtable.md +205 -0
  164. package/data/harnesses/code/skills/design/design-md/library/anthropic-claude.md +203 -0
  165. package/data/harnesses/code/skills/design/design-md/library/apple.md +206 -0
  166. package/data/harnesses/code/skills/design/design-md/library/binance.md +210 -0
  167. package/data/harnesses/code/skills/design/design-md/library/bmw.md +210 -0
  168. package/data/harnesses/code/skills/design/design-md/library/cal.md +209 -0
  169. package/data/harnesses/code/skills/design/design-md/library/clay.md +204 -0
  170. package/data/harnesses/code/skills/design/design-md/library/clickhouse.md +205 -0
  171. package/data/harnesses/code/skills/design/design-md/library/cohere.md +205 -0
  172. package/data/harnesses/code/skills/design/design-md/library/coinbase.md +198 -0
  173. package/data/harnesses/code/skills/design/design-md/library/composio.md +198 -0
  174. package/data/harnesses/code/skills/design/design-md/library/cursor.md +197 -0
  175. package/data/harnesses/code/skills/design/design-md/library/elevenlabs.md +199 -0
  176. package/data/harnesses/code/skills/design/design-md/library/expo.md +200 -0
  177. package/data/harnesses/code/skills/design/design-md/library/figma.md +199 -0
  178. package/data/harnesses/code/skills/design/design-md/library/framer.md +202 -0
  179. package/data/harnesses/code/skills/design/design-md/library/hashicorp.md +205 -0
  180. package/data/harnesses/code/skills/design/design-md/library/ibm.md +205 -0
  181. package/data/harnesses/code/skills/design/design-md/library/intercom.md +204 -0
  182. package/data/harnesses/code/skills/design/design-md/library/kraken.md +200 -0
  183. package/data/harnesses/code/skills/design/design-md/library/linear-app.md +198 -0
  184. package/data/harnesses/code/skills/design/design-md/library/lovable.md +199 -0
  185. package/data/harnesses/code/skills/design/design-md/library/meta.md +204 -0
  186. package/data/harnesses/code/skills/design/design-md/library/minimax.md +204 -0
  187. package/data/harnesses/code/skills/design/design-md/library/mintlify.md +210 -0
  188. package/data/harnesses/code/skills/design/design-md/library/miro.md +209 -0
  189. package/data/harnesses/code/skills/design/design-md/library/mistral-ai.md +201 -0
  190. package/data/harnesses/code/skills/design/design-md/library/mongodb.md +201 -0
  191. package/data/harnesses/code/skills/design/design-md/library/notion.md +204 -0
  192. package/data/harnesses/code/skills/design/design-md/library/nvidia.md +215 -0
  193. package/data/harnesses/code/skills/design/design-md/library/ollama.md +213 -0
  194. package/data/harnesses/code/skills/design/design-md/library/opencode-ai.md +216 -0
  195. package/data/harnesses/code/skills/design/design-md/library/posthog.md +218 -0
  196. package/data/harnesses/code/skills/design/design-md/library/raycast.md +214 -0
  197. package/data/harnesses/code/skills/design/design-md/library/replicate.md +202 -0
  198. package/data/harnesses/code/skills/design/design-md/library/resend.md +203 -0
  199. package/data/harnesses/code/skills/design/design-md/library/revolut.md +203 -0
  200. package/data/harnesses/code/skills/design/design-md/library/runwayml.md +203 -0
  201. package/data/harnesses/code/skills/design/design-md/library/sanity.md +200 -0
  202. package/data/harnesses/code/skills/design/design-md/library/sentry.md +201 -0
  203. package/data/harnesses/code/skills/design/design-md/library/shopify.md +202 -0
  204. package/data/harnesses/code/skills/design/design-md/library/slack.md +202 -0
  205. package/data/harnesses/code/skills/design/design-md/library/spacex.md +206 -0
  206. package/data/harnesses/code/skills/design/design-md/library/spotify.md +206 -0
  207. package/data/harnesses/code/skills/design/design-md/library/stripe.md +196 -0
  208. package/data/harnesses/code/skills/design/design-md/library/supabase.md +196 -0
  209. package/data/harnesses/code/skills/design/design-md/library/superhuman.md +194 -0
  210. package/data/harnesses/code/skills/design/design-md/library/tesla.md +198 -0
  211. package/data/harnesses/code/skills/design/design-md/library/theverge.md +199 -0
  212. package/data/harnesses/code/skills/design/design-md/library/together-ai.md +193 -0
  213. package/data/harnesses/code/skills/design/design-md/library/uber.md +195 -0
  214. package/data/harnesses/code/skills/design/design-md/library/vercel.md +195 -0
  215. package/data/harnesses/code/skills/design/design-md/library/voltagent.md +197 -0
  216. package/data/harnesses/code/skills/design/design-md/library/warp.md +196 -0
  217. package/data/harnesses/code/skills/design/design-md/library/webflow.md +197 -0
  218. package/data/harnesses/code/skills/design/design-md/library/wired.md +197 -0
  219. package/data/harnesses/code/skills/design/design-md/library/wise.md +198 -0
  220. package/data/harnesses/code/skills/design/design-md/library/x-ai.md +199 -0
  221. package/data/harnesses/code/skills/design/design-md/library/zapier.md +198 -0
  222. package/data/harnesses/code/skills/design/design-md/nurix.md +214 -0
  223. package/data/harnesses/code/skills/design/design-md/variants/editorial-luxury.md +184 -0
  224. package/data/harnesses/code/skills/design/design-md/variants/expressive-saas.md +216 -0
  225. package/data/harnesses/code/skills/design/design-md/variants/quiet-premium.md +211 -0
  226. package/data/harnesses/code/skills/design/design-md/variants/terse-dense-operator.md +212 -0
  227. package/data/harnesses/code/skills/design/design-md/variants/warm-approachable.md +216 -0
  228. package/data/harnesses/code/skills/design/references/compose.md +23 -0
  229. package/data/harnesses/code/skills/design/references/content.md +28 -0
  230. package/data/harnesses/code/skills/design/references/emission.md +21 -0
  231. package/data/harnesses/code/skills/design/references/existing-apps.md +27 -0
  232. package/data/harnesses/code/skills/design/references/intake.md +36 -0
  233. package/data/harnesses/code/skills/design/references/object-pass.md +24 -0
  234. package/data/harnesses/code/skills/design/references/ship-gate.md +29 -0
  235. package/data/harnesses/code/skills/design/references/ux-patterns/Readme.md +195 -0
  236. package/data/harnesses/code/skills/design/references/ux-patterns/ai-layer.md +49 -0
  237. package/data/harnesses/code/skills/design/references/ux-patterns/attributes-lifecycle.md +43 -0
  238. package/data/harnesses/code/skills/design/references/ux-patterns/container-choice.md +44 -0
  239. package/data/harnesses/code/skills/design/references/ux-patterns/creation-paths.md +46 -0
  240. package/data/harnesses/code/skills/design/references/ux-patterns/dashboards.md +42 -0
  241. package/data/harnesses/code/skills/design/references/ux-patterns/modes.md +42 -0
  242. package/data/harnesses/code/skills/design/references/ux-patterns/object-model.md +46 -0
  243. package/data/harnesses/code/skills/design/references/ux-patterns/page-archetype.md +48 -0
  244. package/data/harnesses/code/skills/design/references/ux-patterns/promotion-demotion.md +37 -0
  245. package/data/harnesses/code/skills/design/references/ux-patterns/settings.md +42 -0
  246. package/data/harnesses/code/skills/design/references/ux-patterns/standard-pages.md +46 -0
  247. package/data/harnesses/code/skills/design/references/ux-patterns/state-matrix.md +41 -0
  248. package/data/harnesses/code/skills/design/references/ux-patterns/table-mechanics.md +48 -0
  249. package/data/harnesses/code/skills/design/references/ux-patterns/verbs.md +45 -0
  250. package/data/harnesses/code/skills/design/references/ux-patterns/viewer-context.md +40 -0
  251. package/data/harnesses/code/skills/design/scripts/README-validator.md +27 -0
  252. package/data/harnesses/code/skills/design/scripts/select-chart.mjs +596 -0
  253. package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +1198 -0
  254. package/data/harnesses/discussion/agents/discussion-baseline.md +1 -1
  255. package/data/harnesses/discussion/harness.json +1 -1
  256. package/data/harnesses/etna/agents/maintenance.md +1 -1
  257. package/data/harnesses/etna/harness.json +1 -1
  258. package/data/harnesses/hr/CLAUDE.md +1 -1
  259. package/data/harnesses/hr/harness.json +1 -1
  260. package/package.json +1 -1
  261. package/data/harnesses/code/skills/design/data/catalog/rules.csv +0 -137
  262. package/data/harnesses/code/skills/design/data/combinations/Readme.md +0 -33
  263. package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +0 -113
  264. package/data/harnesses/code/skills/design/data/combinations/charts.csv +0 -33
  265. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +0 -448
  266. package/data/harnesses/code/skills/design/data/combinations/icons.csv +0 -157
  267. package/data/harnesses/code/skills/design/data/combinations/landing.csv +0 -38
  268. package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +0 -2001
  269. package/data/harnesses/code/skills/design/data/combinations/manifest.json +0 -330
  270. package/data/harnesses/code/skills/design/data/combinations/motion.csv +0 -10
  271. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +0 -92
  272. package/data/harnesses/code/skills/design/data/combinations/product-router.csv +0 -80
  273. package/data/harnesses/code/skills/design/data/combinations/styles.csv +0 -67
  274. package/data/harnesses/code/skills/design/data/combinations/typography.csv +0 -64
  275. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +0 -106
  276. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +0 -154
  277. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +0 -151
  278. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +0 -139
  279. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +0 -139
  280. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +0 -140
  281. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +0 -140
  282. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +0 -140
  283. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +0 -139
  284. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +0 -139
  285. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +0 -147
  286. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +0 -156
  287. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +0 -138
  288. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +0 -138
  289. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +0 -137
  290. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -148
  291. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -141
  292. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +0 -156
  293. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +0 -140
  294. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +0 -153
  295. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +0 -158
  296. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +0 -156
  297. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -122
  298. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +0 -140
  299. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +0 -139
  300. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +0 -139
  301. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +0 -139
  302. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +0 -153
  303. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +0 -140
  304. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +0 -155
  305. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +0 -140
  306. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +0 -141
  307. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +0 -145
  308. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +0 -142
  309. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +0 -152
  310. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +0 -137
  311. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +0 -140
  312. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +0 -170
  313. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +0 -145
  314. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -137
  315. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +0 -168
  316. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +0 -163
  317. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +0 -147
  318. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +0 -158
  319. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +0 -171
  320. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +0 -166
  321. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +0 -160
  322. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +0 -173
  323. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +0 -145
  324. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +0 -153
  325. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +0 -167
  326. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +0 -140
  327. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +0 -140
  328. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +0 -140
  329. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +0 -130
  330. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +0 -140
  331. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +0 -140
  332. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +0 -139
  333. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +0 -137
  334. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +0 -138
  335. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +0 -158
  336. package/data/harnesses/code/skills/design/data/design-md/nurix.md +0 -138
  337. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +0 -695
  338. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +0 -20
  339. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +0 -1272
  340. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +0 -20
  341. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +0 -1268
  342. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +0 -20
  343. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +0 -1273
  344. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +0 -20
  345. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +0 -1262
  346. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +0 -20
  347. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +0 -1257
  348. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +0 -20
  349. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +0 -1270
  350. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +0 -20
  351. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +0 -1270
  352. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +0 -20
  353. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +0 -1261
  354. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +0 -20
  355. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +0 -1268
  356. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +0 -20
  357. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +0 -1281
  358. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +0 -20
  359. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +0 -1262
  360. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +0 -20
  361. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +0 -1271
  362. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +0 -20
  363. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +0 -1276
  364. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +0 -20
  365. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +0 -1293
  366. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +0 -20
  367. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +0 -1270
  368. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +0 -20
  369. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +0 -1276
  370. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +0 -20
  371. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +0 -1267
  372. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +0 -251
  373. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +0 -20
  374. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +0 -1273
  375. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +0 -20
  376. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +0 -1286
  377. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +0 -20
  378. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +0 -1290
  379. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +0 -20
  380. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +0 -1270
  381. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +0 -23
  382. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +0 -599
  383. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +0 -1272
  384. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +0 -20
  385. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +0 -1267
  386. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +0 -20
  387. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +0 -1289
  388. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +0 -20
  389. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +0 -1258
  390. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +0 -20
  391. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +0 -1284
  392. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +0 -20
  393. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +0 -1291
  394. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +0 -20
  395. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +0 -1263
  396. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +0 -20
  397. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +0 -1270
  398. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +0 -20
  399. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +0 -1261
  400. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +0 -20
  401. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +0 -1209
  402. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +0 -20
  403. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +0 -1265
  404. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +0 -20
  405. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +0 -1253
  406. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +0 -20
  407. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +0 -1268
  408. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +0 -20
  409. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +0 -1261
  410. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +0 -20
  411. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +0 -1263
  412. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +0 -20
  413. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +0 -1266
  414. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +0 -23
  415. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +0 -623
  416. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +0 -1276
  417. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +0 -20
  418. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +0 -1264
  419. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +0 -20
  420. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +0 -1268
  421. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +0 -20
  422. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +0 -1270
  423. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +0 -20
  424. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +0 -1288
  425. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +0 -20
  426. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +0 -1272
  427. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +0 -20
  428. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +0 -1265
  429. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +0 -20
  430. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +0 -1287
  431. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +0 -20
  432. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +0 -1286
  433. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +0 -20
  434. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +0 -1285
  435. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +0 -20
  436. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +0 -1287
  437. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +0 -20
  438. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +0 -1278
  439. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +0 -20
  440. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +0 -1269
  441. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +0 -20
  442. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +0 -1285
  443. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +0 -20
  444. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +0 -1303
  445. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +0 -20
  446. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +0 -1274
  447. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +0 -20
  448. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +0 -1296
  449. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +0 -23
  450. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +0 -612
  451. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +0 -1282
  452. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +0 -20
  453. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +0 -1287
  454. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +0 -20
  455. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +0 -1268
  456. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +0 -20
  457. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +0 -1276
  458. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +0 -20
  459. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +0 -1273
  460. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +0 -20
  461. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +0 -1296
  462. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +0 -20
  463. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +0 -1286
  464. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +0 -249
  465. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +0 -249
  466. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +0 -225
  467. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +0 -249
  468. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +0 -224
  469. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +0 -249
  470. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +0 -88
  471. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +0 -132
  472. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +0 -76
  473. package/data/harnesses/code/skills/design/references/create.md +0 -173
  474. package/data/harnesses/code/skills/design/references/diagnosis.md +0 -108
  475. package/data/harnesses/code/skills/design/references/hard-rules.md +0 -177
  476. package/data/harnesses/code/skills/design/references/improve.md +0 -116
  477. package/data/harnesses/code/skills/design/references/interaction.md +0 -77
  478. package/data/harnesses/code/skills/design/references/introspect.md +0 -100
  479. package/data/harnesses/code/skills/design/references/lifecycle.md +0 -73
  480. package/data/harnesses/code/skills/design/references/personality.md +0 -575
  481. package/data/harnesses/code/skills/design/references/structure.md +0 -159
  482. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +0 -1312
  483. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +0 -1498
  484. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +0 -551
  485. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +0 -1092
  486. package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +0 -26
  487. package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +0 -65
  488. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +0 -538
  489. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +0 -312
  490. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +0 -890
  491. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +0 -769
  492. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +0 -675
  493. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +0 -164
  494. 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();