@nurix/etna 0.18.20 → 0.18.22

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 (272) hide show
  1. package/README.md +1 -1
  2. package/data/harness-index.json +1 -1
  3. package/data/harnesses/code/CLAUDE.md +3 -3
  4. package/data/harnesses/code/agents/brainstorm.md +1 -1
  5. package/data/harnesses/code/agents/maintenance.md +8 -7
  6. package/data/harnesses/code/harness.json +2 -2
  7. package/data/harnesses/code/rules/database.md +38 -0
  8. package/data/harnesses/code/rules/express.md +28 -0
  9. package/data/harnesses/code/rules/frontend.md +14 -0
  10. package/data/harnesses/code/rules/general.md +2 -40
  11. package/data/harnesses/code/rules/nomenclature.md +1 -8
  12. package/data/harnesses/code/rules/nustack.md +150 -0
  13. package/data/harnesses/code/skills/brainstorm/README.md +4 -4
  14. package/data/harnesses/code/skills/brainstorm/SKILL.md +5 -3
  15. package/data/harnesses/code/skills/brainstorm/exit-artifacts.md +15 -11
  16. package/data/harnesses/code/skills/brainstorm/templates/source-of-truth.md +5 -5
  17. package/data/harnesses/code/skills/design/SKILL.md +14 -11
  18. package/data/harnesses/code/skills/design/data/catalog/rules.csv +4 -4
  19. package/data/harnesses/code/skills/design/data/combinations/Readme.md +29 -0
  20. package/data/harnesses/code/skills/design/data/combinations/charts.csv +33 -0
  21. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +437 -0
  22. package/data/harnesses/code/skills/design/data/combinations/icons.csv +157 -0
  23. package/data/harnesses/code/skills/design/data/combinations/landing.csv +38 -0
  24. package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +2001 -0
  25. package/data/harnesses/code/skills/design/data/combinations/manifest.json +283 -0
  26. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +49 -0
  27. package/data/harnesses/code/skills/design/data/combinations/product-router.csv +80 -0
  28. package/data/harnesses/code/skills/design/data/combinations/styles.csv +54 -0
  29. package/data/harnesses/code/skills/design/data/combinations/typography.csv +61 -0
  30. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +27 -238
  31. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +7 -88
  32. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +1 -82
  33. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +4 -85
  34. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +5 -86
  35. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +4 -87
  36. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +7 -86
  37. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +4 -84
  38. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +5 -86
  39. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +3 -84
  40. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +2 -82
  41. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +10 -91
  42. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +2 -84
  43. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +3 -85
  44. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +7 -90
  45. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -80
  46. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -80
  47. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +5 -85
  48. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +2 -82
  49. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +5 -86
  50. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +9 -91
  51. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +9 -89
  52. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -115
  53. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -85
  54. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +7 -88
  55. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +3 -84
  56. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +4 -85
  57. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +8 -88
  58. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +8 -88
  59. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +4 -85
  60. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -84
  61. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +7 -88
  62. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +5 -86
  63. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +6 -87
  64. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +9 -91
  65. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +5 -88
  66. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +9 -89
  67. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +4 -119
  68. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +4 -84
  69. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -82
  70. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +3 -86
  71. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +13 -94
  72. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +2 -82
  73. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -88
  74. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +13 -94
  75. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +4 -86
  76. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +7 -88
  77. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +6 -88
  78. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +5 -86
  79. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +6 -86
  80. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +12 -93
  81. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +11 -91
  82. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +8 -88
  83. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +2 -82
  84. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +11 -126
  85. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +8 -88
  86. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +3 -83
  87. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +10 -91
  88. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +7 -90
  89. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +6 -88
  90. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +9 -89
  91. package/data/harnesses/code/skills/design/data/design-md/nurix.md +4 -87
  92. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +580 -0
  93. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +20 -0
  94. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +964 -0
  95. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +20 -0
  96. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +960 -0
  97. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +20 -0
  98. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +965 -0
  99. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +20 -0
  100. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +954 -0
  101. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +20 -0
  102. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +949 -0
  103. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +20 -0
  104. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +962 -0
  105. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +20 -0
  106. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +958 -0
  107. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +20 -0
  108. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +953 -0
  109. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +20 -0
  110. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +960 -0
  111. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +20 -0
  112. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +973 -0
  113. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +20 -0
  114. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +954 -0
  115. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +20 -0
  116. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +963 -0
  117. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +20 -0
  118. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +968 -0
  119. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +20 -0
  120. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +981 -0
  121. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +20 -0
  122. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +962 -0
  123. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +20 -0
  124. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +964 -0
  125. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +20 -0
  126. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +959 -0
  127. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +251 -0
  128. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +20 -0
  129. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +965 -0
  130. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +20 -0
  131. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +978 -0
  132. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +20 -0
  133. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +982 -0
  134. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +20 -0
  135. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +958 -0
  136. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +23 -0
  137. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +589 -0
  138. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +964 -0
  139. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +20 -0
  140. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +955 -0
  141. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +20 -0
  142. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +977 -0
  143. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +20 -0
  144. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +950 -0
  145. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +20 -0
  146. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +976 -0
  147. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +20 -0
  148. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +979 -0
  149. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +20 -0
  150. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +955 -0
  151. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +20 -0
  152. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +958 -0
  153. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +20 -0
  154. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +953 -0
  155. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +20 -0
  156. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +904 -0
  157. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +20 -0
  158. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +957 -0
  159. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +20 -0
  160. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +945 -0
  161. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +20 -0
  162. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +960 -0
  163. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +20 -0
  164. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +953 -0
  165. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +20 -0
  166. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +955 -0
  167. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +20 -0
  168. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +958 -0
  169. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +23 -0
  170. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +613 -0
  171. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +968 -0
  172. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +20 -0
  173. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +956 -0
  174. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +20 -0
  175. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +960 -0
  176. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +20 -0
  177. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +958 -0
  178. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +20 -0
  179. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +980 -0
  180. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +20 -0
  181. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +964 -0
  182. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +20 -0
  183. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +957 -0
  184. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +20 -0
  185. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +979 -0
  186. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +20 -0
  187. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +974 -0
  188. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +20 -0
  189. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +973 -0
  190. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +20 -0
  191. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +979 -0
  192. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +20 -0
  193. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +966 -0
  194. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +20 -0
  195. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +961 -0
  196. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +20 -0
  197. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +977 -0
  198. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +20 -0
  199. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +991 -0
  200. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +20 -0
  201. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +966 -0
  202. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +20 -0
  203. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +988 -0
  204. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +23 -0
  205. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +602 -0
  206. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +974 -0
  207. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +20 -0
  208. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +979 -0
  209. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +20 -0
  210. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +960 -0
  211. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +20 -0
  212. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +968 -0
  213. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +20 -0
  214. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +965 -0
  215. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +20 -0
  216. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +988 -0
  217. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +20 -0
  218. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +978 -0
  219. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +1 -1
  220. package/data/harnesses/code/skills/design/references/create.md +14 -9
  221. package/data/harnesses/code/skills/design/references/diagnosis.md +1 -1
  222. package/data/harnesses/code/skills/design/references/hard-rules.md +4 -4
  223. package/data/harnesses/code/skills/design/references/improve.md +11 -9
  224. package/data/harnesses/code/skills/design/references/introspect.md +1 -1
  225. package/data/harnesses/code/skills/design/references/personality.md +337 -413
  226. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +1032 -0
  227. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -296
  228. package/data/harnesses/code/skills/design/scripts/{detector.mjs → lib/checks.mjs} +145 -140
  229. package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +26 -0
  230. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +105 -37
  231. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +60 -73
  232. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +1 -1
  233. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +1 -1
  234. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +205 -37
  235. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +68 -0
  236. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +314 -0
  237. package/data/harnesses/code/skills/documentation/SKILL.md +61 -17
  238. package/data/harnesses/code/skills/documentation/references/examples.md +41 -0
  239. package/data/harnesses/code/skills/documentation/references/style-guide.md +14 -9
  240. package/data/harnesses/code/skills/documentation/references/templates.md +306 -52
  241. package/data/harnesses/etna/CLAUDE.md +3 -3
  242. package/data/harnesses/etna/agents/brainstorm.md +1 -1
  243. package/data/harnesses/etna/agents/maintenance.md +8 -7
  244. package/data/harnesses/etna/harness.json +2 -2
  245. package/data/harnesses/etna/rules/general.md +2 -40
  246. package/data/harnesses/etna/rules/nomenclature.md +1 -8
  247. package/data/harnesses/etna/skills/brainstorm/README.md +4 -4
  248. package/data/harnesses/etna/skills/brainstorm/SKILL.md +5 -3
  249. package/data/harnesses/etna/skills/brainstorm/exit-artifacts.md +15 -11
  250. package/data/harnesses/etna/skills/brainstorm/templates/source-of-truth.md +5 -5
  251. package/data/harnesses/etna/skills/documentation/SKILL.md +61 -17
  252. package/data/harnesses/etna/skills/documentation/references/examples.md +41 -0
  253. package/data/harnesses/etna/skills/documentation/references/style-guide.md +14 -9
  254. package/data/harnesses/etna/skills/documentation/references/templates.md +306 -52
  255. package/data/harnesses/mobile/CLAUDE.md +3 -3
  256. package/data/harnesses/mobile/agents/brainstorm.md +1 -1
  257. package/data/harnesses/mobile/agents/maintenance.md +8 -7
  258. package/data/harnesses/mobile/harness.json +2 -2
  259. package/data/harnesses/mobile/rules/database.md +38 -0
  260. package/data/harnesses/mobile/rules/general.md +2 -40
  261. package/data/harnesses/mobile/rules/nomenclature.md +1 -8
  262. package/data/harnesses/mobile/rules/nustack.md +150 -0
  263. package/data/harnesses/mobile/skills/brainstorm/README.md +4 -4
  264. package/data/harnesses/mobile/skills/brainstorm/SKILL.md +5 -3
  265. package/data/harnesses/mobile/skills/brainstorm/exit-artifacts.md +15 -11
  266. package/data/harnesses/mobile/skills/brainstorm/templates/source-of-truth.md +5 -5
  267. package/data/harnesses/mobile/skills/documentation/SKILL.md +61 -17
  268. package/data/harnesses/mobile/skills/documentation/references/examples.md +41 -0
  269. package/data/harnesses/mobile/skills/documentation/references/style-guide.md +14 -9
  270. package/data/harnesses/mobile/skills/documentation/references/templates.md +306 -52
  271. package/package.json +1 -1
  272. package/data/harnesses/code/skills/design/scripts/hook-config.json +0 -11
@@ -1,19 +1,35 @@
1
1
  #!/usr/bin/env node
2
- // skills/design/scripts/derive-personality.mjs — design.md parser, coverage gate (--check), and token emitter (--emit) for the operator-UI personality system (D29, D6).
3
-
4
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ // skills/design/scripts/derive-personality.mjs — design-tokens artifact resolver, coverage gate (--check; SCHEMA.md rules 1–12), and CSS emitter (--emit) for the operator-UI personality system (D29, D6).
3
+ //
4
+ // The reference consumer of data/design-tokens/SCHEMA.md: resolves an artifact directory
5
+ // (design.tokens.json + design.resolver.json [+ design.tokens.dark.json]) through the resolver's
6
+ // deterministic order — concatenate the set files, overlay the selected theme context, resolve
7
+ // aliases — then validates (rules 1–12) or emits personality.css / globals.css / the <html> attributes.
8
+ //
9
+ // Usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir>] [--theme light|dark]
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
12
  import { dirname, join } from "node:path";
6
13
 
7
- // The 26 visual primitives, in emission order (names are law; no leading --).
8
- const PRIMITIVES = [
9
- "radius", "shadow-intensity", "border-weight", "density-gap", "density-inset",
10
- "font-sans", "font-mono", "font-weight-normal", "font-weight-strong", "heading-tracking",
11
- "duration-unit", "duration-fast", "duration-normal", "duration-slow", "ease-default",
12
- "contrast-boost",
13
- "icon-stroke", "icon-size-base",
14
- "sidebar-width-expanded", "sidebar-width-collapsed", "topbar-height", "side-panel-width",
15
- "side-panel-width-rich", "modal-width-max", "form-max-width", "table-row-height",
16
- ];
14
+ const EXT = "com.nurix.design";
15
+
16
+ // The 26 visual primitives and their DTCG $type, in emission order (leaf names are law; no leading --).
17
+ const PRIMITIVE_TYPES = new Map([
18
+ ["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
19
+ ["density-gap", "number"], ["density-inset", "number"],
20
+ ["font-sans", "fontFamily"], ["font-mono", "fontFamily"],
21
+ ["font-weight-normal", "fontWeight"], ["font-weight-strong", "fontWeight"],
22
+ ["heading-tracking", "number"],
23
+ ["duration-unit", "duration"], ["duration-fast", "duration"], ["duration-normal", "duration"],
24
+ ["duration-slow", "duration"], ["ease-default", "cubicBezier"],
25
+ ["contrast-boost", "number"],
26
+ ["icon-stroke", "number"], ["icon-size-base", "dimension"],
27
+ ["sidebar-width-expanded", "dimension"], ["sidebar-width-collapsed", "dimension"],
28
+ ["topbar-height", "dimension"], ["side-panel-width", "dimension"],
29
+ ["side-panel-width-rich", "dimension"], ["modal-width-max", "dimension"],
30
+ ["form-max-width", "dimension"], ["table-row-height", "dimension"],
31
+ ]);
32
+ const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
17
33
 
18
34
  // The 17 semantic color tokens, in emission order.
19
35
  const SEMANTIC_TOKENS = [
@@ -32,312 +48,574 @@ const SUBSTRATE_TOKENS = [
32
48
 
33
49
  const ALL_TOKENS = [...SEMANTIC_TOKENS, ...SUBSTRATE_TOKENS];
34
50
 
51
+ const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption"];
35
52
  const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
36
53
  const LEGAL_FEEDBACK = ["subtle", "minimal"];
37
54
  const LEGAL_DENSITY = ["spacious", "standard", "dense"];
38
55
  const LEGAL_PROVENANCE = ["project", "extracted", "library", "default"];
56
+ const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography"];
57
+ const LEGAL_THEMES = ["light", "dark"];
58
+ const MANIFEST_KEYS = ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"];
39
59
 
40
60
  // Shadow-level names ranked by increasing intensity, for the ordering check.
41
61
  const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7 };
42
62
 
43
- // Strip a trailing #-comment while respecting quoted spans (hex colors are quoted, so an unquoted # is always a comment).
44
- function stripComment(line) {
45
- let inSingle = false, inDouble = false;
46
- for (let i = 0; i < line.length; i++) {
47
- const c = line[i];
48
- if (c === '"' && !inSingle) inDouble = !inDouble;
49
- else if (c === "'" && !inDouble) inSingle = !inSingle;
50
- else if (c === "#" && !inSingle && !inDouble) return line.slice(0, i);
51
- }
52
- return line;
63
+ // ---------------------------------------------------------------------------
64
+ // CSS value parsers — the parse-back authority (rule 9); byte-compatible with convert-design-md.mjs
65
+ // ---------------------------------------------------------------------------
66
+
67
+ // Round to at most 5 decimals (the srgb component precision).
68
+ function round5(n) {
69
+ return Math.round(n * 1e5) / 1e5;
53
70
  }
54
71
 
55
- // Remove one layer of matching surrounding quotes from a scalar.
72
+ // Strip one layer of matching surrounding quotes and unescape \" within double quotes.
56
73
  function unquote(raw) {
57
74
  const s = raw.trim();
58
- if (s.length >= 2 && ((s[0] === '"' && s.at(-1) === '"') || (s[0] === "'" && s.at(-1) === "'"))) {
59
- return s.slice(1, -1);
60
- }
75
+ if (s.length >= 2 && s[0] === '"' && s.at(-1) === '"') return s.slice(1, -1).replaceAll('\\"', '"');
76
+ if (s.length >= 2 && s[0] === "'" && s.at(-1) === "'") return s.slice(1, -1);
61
77
  return s;
62
78
  }
63
79
 
64
- // Split a string on a delimiter at bracket-depth 0 and outside quotes (handles nested [] {} and quoted commas).
80
+ // Split on a delimiter at bracket-depth 0 and outside quotes.
65
81
  function splitTopLevel(str, delim) {
66
82
  const parts = [];
67
- let depth = 0, inSingle = false, inDouble = false, cur = "";
68
- for (const c of str) {
69
- if (c === '"' && !inSingle) { inDouble = !inDouble; cur += c; }
83
+ let depth = 0, inDouble = false, inSingle = false, cur = "";
84
+ for (let i = 0; i < str.length; i++) {
85
+ const c = str[i];
86
+ if (c === '"' && !inSingle && str[i - 1] !== "\\") { inDouble = !inDouble; cur += c; }
70
87
  else if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; }
71
- else if (!inSingle && !inDouble && (c === "[" || c === "{")) { depth++; cur += c; }
72
- else if (!inSingle && !inDouble && (c === "]" || c === "}")) { depth--; cur += c; }
73
- else if (!inSingle && !inDouble && c === delim && depth === 0) { parts.push(cur); cur = ""; }
88
+ else if (!inDouble && !inSingle && (c === "[" || c === "{" || c === "(")) { depth++; cur += c; }
89
+ else if (!inDouble && !inSingle && (c === "]" || c === "}" || c === ")")) { depth--; cur += c; }
90
+ else if (!inDouble && !inSingle && depth === 0 && c === delim) { parts.push(cur); cur = ""; }
74
91
  else cur += c;
75
92
  }
76
93
  parts.push(cur);
77
94
  return parts;
78
95
  }
79
96
 
80
- // Parse a scalar / inline array / inline object into a JS value.
81
- function parseScalar(raw) {
82
- const s = raw.trim();
83
- if (s.startsWith("[")) return parseInlineArray(s);
84
- if (s.startsWith("{")) return parseInlineObject(s);
85
- return unquote(s);
86
- }
87
-
88
- // Parse an inline [a, b, c] array into a JS array of parsed scalars.
89
- function parseInlineArray(s) {
90
- const inner = s.slice(s.indexOf("[") + 1, s.lastIndexOf("]"));
91
- if (inner.trim() === "") return [];
92
- return splitTopLevel(inner, ",").map((x) => parseScalar(x)).filter((v) => !(typeof v === "string" && v === ""));
93
- }
94
-
95
- // Parse an inline { k: v, ... } object into a plain JS object.
96
- function parseInlineObject(s) {
97
- const inner = s.slice(s.indexOf("{") + 1, s.lastIndexOf("}"));
98
- const obj = {};
99
- for (const part of splitTopLevel(inner, ",")) {
100
- if (part.trim() === "") continue;
101
- const ci = part.indexOf(":");
102
- if (ci < 0) continue;
103
- obj[part.slice(0, ci).trim()] = parseScalar(part.slice(ci + 1));
104
- }
105
- return obj;
106
- }
107
-
108
- // Turn a block's raw text into meaningful {indent, content} nodes (comment-stripped, blank-dropped).
109
- function toNodes(text) {
110
- const nodes = [];
111
- for (const rawLine of text.split("\n")) {
112
- const line = stripComment(rawLine);
113
- if (line.trim() === "") continue;
114
- const indent = line.length - line.trimStart().length;
115
- nodes.push({ indent, content: line.trim() });
116
- }
117
- return nodes;
118
- }
119
-
120
- // Recursively build a nested map from indentation nodes (tolerant mini-YAML for 2-space maps).
121
- function buildMap(nodes, start, indent) {
122
- const obj = {};
123
- let i = start;
124
- while (i < nodes.length) {
125
- const node = nodes[i];
126
- if (node.indent < indent) break;
127
- if (node.indent > indent) { i++; continue; }
128
- const ci = node.content.indexOf(":");
129
- if (ci < 0) { i++; continue; }
130
- const key = node.content.slice(0, ci).trim();
131
- const rest = node.content.slice(ci + 1).trim();
132
- if (rest === "") {
133
- if (i + 1 < nodes.length && nodes[i + 1].indent > indent) {
134
- const [child, next] = buildMap(nodes, i + 1, nodes[i + 1].indent);
135
- obj[key] = child;
136
- i = next;
137
- } else {
138
- obj[key] = {};
139
- i++;
140
- }
141
- } else {
142
- obj[key] = parseScalar(rest);
143
- i++;
144
- }
97
+ // Parse any corpus color string (oklch / #rrggbb / #rrggbbaa / rgb() / rgba()) into a typed DTCG color $value.
98
+ function parseColorCss(css) {
99
+ const s = css.trim();
100
+ let m = /^oklch\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s);
101
+ if (m) {
102
+ const value = { colorSpace: "oklch", components: [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])] };
103
+ if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
104
+ return value;
105
+ }
106
+ m = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
107
+ if (m) {
108
+ const bytes = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
109
+ const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex: `#${m[1].toLowerCase()}` };
110
+ if (m[2] !== undefined) value.alpha = round5(parseInt(m[2], 16) / 255);
111
+ return value;
145
112
  }
146
- return [obj, i];
113
+ m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
114
+ if (m) {
115
+ const bytes = [m[1], m[2], m[3]].map(Number);
116
+ const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
117
+ const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
118
+ if (m[4] !== undefined) value.alpha = parseFloat(m[4]);
119
+ return value;
120
+ }
121
+ throw new Error(`unparseable color: ${JSON.stringify(css)}`);
147
122
  }
148
123
 
149
- // Parse a fenced-block body into a nested object via the mini-YAML parser.
150
- function parseBlock(text) {
151
- const [obj] = buildMap(toNodes(text), 0, 0);
152
- return obj;
124
+ // Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
125
+ function parseTrackingCss(css) {
126
+ const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
127
+ if (!m) throw new Error(`unparseable tracking: ${JSON.stringify(css)}`);
128
+ return parseFloat(m[1]);
153
129
  }
154
130
 
155
- // Extract the body of the fenced block whose info-string ends with the given label (e.g. "design-primitives").
156
- function extractFencedBlock(source, label) {
157
- const lines = source.split("\n");
158
- const openRe = new RegExp("^`{3,}\\s*yaml\\s+" + label + "\\s*$");
159
- const closeRe = /^`{3,}\s*$/;
160
- let capturing = false;
161
- const body = [];
162
- for (const line of lines) {
163
- if (!capturing) {
164
- if (openRe.test(line.trim())) capturing = true;
165
- continue;
131
+ // Parse "cubic-bezier(x1,y1,x2,y2)" into its four numbers.
132
+ function parseCubicBezierCss(css) {
133
+ const m = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/.exec(css.trim());
134
+ if (!m) throw new Error(`unparseable cubic-bezier: ${JSON.stringify(css)}`);
135
+ return [1, 2, 3, 4].map((i) => parseFloat(m[i]));
136
+ }
137
+
138
+ // Parse a font stack string into its array of unquoted family names.
139
+ function parseFontStackCss(css) {
140
+ return splitTopLevel(css, ",").map((p) => unquote(p)).filter((p) => p !== "");
141
+ }
142
+
143
+ // Parse a ramp letterSpacing string ("+0.02em" | "-0.28px" | "0") into an em multiplier, px scaled by fontSize.
144
+ function parseLetterSpacingCss(css, fontSizePx) {
145
+ const m = /^([+-]?\d*\.?\d+)(em|px)?$/.exec(css.trim());
146
+ if (!m) throw new Error(`unparseable letterSpacing: ${JSON.stringify(css)}`);
147
+ const n = parseFloat(m[1]);
148
+ return m[2] === "px" ? round5(n / fontSizePx) : n;
149
+ }
150
+
151
+ function parseDimensionCss(css) {
152
+ const m = /^(-?\d*\.?\d+)px$/.exec(css.trim());
153
+ if (!m) throw new Error(`unparseable dimension: ${JSON.stringify(css)}`);
154
+ return { value: parseFloat(m[1]), unit: "px" };
155
+ }
156
+
157
+ function parseDurationCss(css) {
158
+ const m = /^(\d*\.?\d+)ms$/.exec(css.trim());
159
+ if (!m) throw new Error(`unparseable duration: ${JSON.stringify(css)}`);
160
+ return { value: parseFloat(m[1]), unit: "ms" };
161
+ }
162
+
163
+ function parseNumberCss(css) {
164
+ const n = parseFloat(css.trim());
165
+ if (Number.isNaN(n) || !/^[+-]?\d*\.?\d+$/.test(css.trim())) throw new Error(`unparseable number: ${JSON.stringify(css)}`);
166
+ return n;
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Serialization — the schema's deterministic byte forms for tokens without preserved .css
171
+ // ---------------------------------------------------------------------------
172
+
173
+ // Serialize a concrete $value to its deterministic CSS byte form (the fallback when .css is absent).
174
+ function serializeValue(type, value) {
175
+ if (type === "dimension" || type === "duration") return `${value.value}${value.unit}`;
176
+ if (type === "number" || type === "fontWeight") return String(value);
177
+ if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
178
+ if (type === "fontFamily") {
179
+ return (Array.isArray(value) ? value : [value]).map((f) => (/\s/.test(f) ? `"${f}"` : f)).join(", ");
180
+ }
181
+ if (type === "color") {
182
+ if (value.colorSpace === "srgb" && value.hex) return value.hex;
183
+ return `oklch(${value.components.join(" ")}${value.alpha !== undefined ? ` / ${value.alpha}` : ""})`;
184
+ }
185
+ return String(value);
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Artifact loading & resolution (SCHEMA.md resolver section — deterministic order)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ // True when a $value is an alias reference string ("{path.to.token}").
193
+ function isAlias(v) {
194
+ return typeof v === "string" && /^\{[^}]+\}$/.test(v);
195
+ }
196
+
197
+ // True when a node is a token (an object carrying $type).
198
+ function isToken(node) {
199
+ return node !== null && typeof node === "object" && !Array.isArray(node) && "$type" in node;
200
+ }
201
+
202
+ // Collect every token in a tree as a path → token map ($-prefixed keys are metadata, not groups).
203
+ function collectTokens(root) {
204
+ const tokens = new Map();
205
+ const walk = (node, path) => {
206
+ if (isToken(node)) { tokens.set(path.join("."), node); return; }
207
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
208
+ for (const [k, v] of Object.entries(node)) if (!k.startsWith("$")) walk(v, [...path, k]);
166
209
  }
167
- if (closeRe.test(line.trim())) return body.join("\n");
168
- body.push(line);
169
- }
170
- return capturing ? body.join("\n") : null;
171
- }
172
-
173
- // Parse the leading --- frontmatter into a flat object.
174
- function parseFrontmatter(source) {
175
- const m = /^---\s*\n([\s\S]*?)\n---\s*(\n|$)/.exec(source);
176
- if (!m) return null;
177
- const obj = {};
178
- for (const rawLine of m[1].split("\n")) {
179
- const line = stripComment(rawLine);
180
- const ci = line.indexOf(":");
181
- if (ci < 0) continue;
182
- obj[line.slice(0, ci).trim()] = unquote(line.slice(ci + 1));
183
- }
184
- return obj;
185
- }
186
-
187
- // Parse a whole design.md into its three parsed parts.
188
- function parseDesignMd(source) {
189
- const frontmatter = parseFrontmatter(source);
190
- const primitivesText = extractFencedBlock(source, "design-primitives");
191
- const manifestText = extractFencedBlock(source, "lever-manifest");
192
- return {
193
- frontmatter,
194
- primitives: primitivesText == null ? null : parseBlock(primitivesText),
195
- manifest: manifestText == null ? null : parseBlock(manifestText),
196
210
  };
211
+ walk(root, []);
212
+ return tokens;
213
+ }
214
+
215
+ // Overlay one modifier file onto the base at token-path granularity; an overlay may only
216
+ // override paths that exist in the base, never introduce new ones.
217
+ function overlayTree(base, overlay, file, errors, path = []) {
218
+ for (const [key, value] of Object.entries(overlay)) {
219
+ if (key.startsWith("$")) continue;
220
+ const here = [...path, key];
221
+ if (!(key in base)) {
222
+ errors.push(`rule 1 — resolver overlay ${file}: introduces ${here.join(".")}, absent from the base set`);
223
+ continue;
224
+ }
225
+ if (isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
226
+ base[key] = value;
227
+ } else {
228
+ overlayTree(base[key], value, file, errors, here);
229
+ }
230
+ }
197
231
  }
198
232
 
199
- // True when a parsed leaf is a bare "hint:" pending judgment.
200
- function isHint(value) {
201
- return typeof value === "string" && value.trim().startsWith("hint:");
233
+ // Concatenate a later set file onto the accumulated base (a later file overrides per token path; new paths are legal here).
234
+ function concatTree(base, add) {
235
+ for (const [key, value] of Object.entries(add)) {
236
+ if (key.startsWith("$")) continue;
237
+ if (!(key in base) || isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
238
+ base[key] = value;
239
+ } else {
240
+ concatTree(base[key], value);
241
+ }
242
+ }
202
243
  }
203
244
 
204
- // True when a leaf is present as a non-empty scalar.
205
- function isFilled(value) {
206
- return typeof value === "string" && value.trim() !== "";
245
+ // Load and resolve one artifact directory: parse files (rule 1), concatenate sets, build one
246
+ // resolved view per theme context. Returns null when the artifact is unreadable.
247
+ function loadArtifact(dir, errors) {
248
+ const readJson = (name) => {
249
+ const path = join(dir, name);
250
+ if (!existsSync(path)) { errors.push(`rule 1 — ${name}: missing`); return null; }
251
+ try {
252
+ return JSON.parse(readFileSync(path, "utf8"));
253
+ } catch (e) {
254
+ errors.push(`rule 1 — ${name}: unparseable JSON (${e.message})`);
255
+ return null;
256
+ }
257
+ };
258
+
259
+ const base = readJson("design.tokens.json");
260
+ const resolver = readJson("design.resolver.json");
261
+ if (base === null || resolver === null) return null;
262
+
263
+ if (resolver.schema !== "design-resolver/v1") {
264
+ errors.push(`rule 1 — design.resolver.json: schema must be "design-resolver/v1" (found ${JSON.stringify(resolver.schema)})`);
265
+ }
266
+ if (!Array.isArray(resolver.sets) || resolver.sets.length === 0) {
267
+ errors.push("rule 1 — design.resolver.json: sets must be a non-empty array");
268
+ return null;
269
+ }
270
+ const themeModifier = (resolver.modifiers ?? []).find((m) => m?.name === "theme");
271
+ for (const m of resolver.modifiers ?? []) {
272
+ if (m?.name !== "theme") errors.push(`rule 1 — design.resolver.json: unknown modifier ${JSON.stringify(m?.name)} (exactly one modifier today: theme)`);
273
+ }
274
+ if (!themeModifier) {
275
+ errors.push("rule 1 — design.resolver.json: missing the theme modifier");
276
+ return null;
277
+ }
278
+ if (themeModifier.default !== "light") errors.push("rule 1 — design.resolver.json: theme default must be light (light-first)");
279
+ if (!Array.isArray(themeModifier.contexts?.light) || themeModifier.contexts.light.length !== 0) {
280
+ errors.push("rule 1 — design.resolver.json: the light context must be the empty overlay [] (the base set is the light rendering)");
281
+ }
282
+
283
+ // Every file the resolver references exists beside it — and parses.
284
+ const files = new Map([["design.tokens.json", base]]);
285
+ const referenced = new Set(["design.tokens.json"]);
286
+ const loadReferenced = (name, where) => {
287
+ referenced.add(name);
288
+ if (files.has(name)) return;
289
+ const doc = readJson(name);
290
+ if (doc === null) errors.push(`rule 1 — resolver ${where} references ${name}`);
291
+ else files.set(name, doc);
292
+ };
293
+ for (const set of resolver.sets) {
294
+ for (const name of set?.values ?? []) loadReferenced(name, `set "${set.name}"`);
295
+ }
296
+ for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
297
+ for (const name of names ?? []) loadReferenced(name, `theme context "${context}"`);
298
+ }
299
+
300
+ // The dark file exists exactly when the resolver's dark context names it.
301
+ const darkNamed = (themeModifier.contexts?.dark ?? []).includes("design.tokens.dark.json");
302
+ const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
303
+ if (darkNamed && !darkExists) errors.push("rule 1 — design.tokens.dark.json: named by the resolver's dark context but absent");
304
+ if (!darkNamed && darkExists) errors.push("rule 1 — design.tokens.dark.json: present but not named by a resolver dark context");
305
+
306
+ // Resolution: concatenate the set files in array order, then one view per theme context.
307
+ const merged = {};
308
+ for (const set of resolver.sets) {
309
+ for (const name of set?.values ?? []) {
310
+ if (files.has(name)) concatTree(merged, files.get(name));
311
+ }
312
+ }
313
+ const views = new Map();
314
+ for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
315
+ const view = structuredClone(merged);
316
+ for (const name of names ?? []) {
317
+ if (files.has(name)) overlayTree(view, files.get(name), name, errors);
318
+ }
319
+ views.set(context, view);
320
+ }
321
+
322
+ return { base, resolver, files, views, payload: base?.$extensions?.[EXT] };
323
+ }
324
+
325
+ // Follow an alias chain to its final concrete token; records rule-6 errors (dangling refs, cycles, type mismatches).
326
+ function resolveAlias(ref, tokens, expectedType, context, errors) {
327
+ const seen = new Set();
328
+ let cur = ref;
329
+ while (isAlias(cur)) {
330
+ const path = cur.slice(1, -1);
331
+ if (seen.has(path)) { errors.push(`rule 6 — ${context}: alias cycle through {${path}}`); return null; }
332
+ seen.add(path);
333
+ const target = tokens.get(path);
334
+ if (!target) { errors.push(`rule 6 — ${context}: alias {${path}} does not resolve`); return null; }
335
+ if (expectedType && target.$type !== expectedType) {
336
+ errors.push(`rule 6 — ${context}: alias {${path}} resolves to $type ${target.$type} (want ${expectedType})`);
337
+ return null;
338
+ }
339
+ if (!isAlias(target.$value)) return target;
340
+ cur = target.$value;
341
+ }
342
+ return null;
343
+ }
344
+
345
+ // The emitted CSS string for a token path: follow aliases, prefer the preserved .css, fall back
346
+ // to the schema's deterministic serialization.
347
+ function cssFor(path, tokens, errors) {
348
+ const token = tokens.get(path);
349
+ if (!token) { errors.push(`emit — ${path}: token not found`); return ""; }
350
+ let target = token;
351
+ if (isAlias(token.$value)) {
352
+ target = resolveAlias(token.$value, tokens, token.$type, path, errors);
353
+ if (!target) return "";
354
+ }
355
+ const css = target.$extensions?.[EXT]?.css;
356
+ if (typeof css === "string") return css;
357
+ return serializeValue(target.$type, target.$value);
207
358
  }
208
359
 
209
- // True when an array of numeric-ish values is strictly ascending.
210
- function isAscending(arr) {
211
- const nums = arr.map((v) => parseFloat(v));
212
- if (nums.some((n) => Number.isNaN(n))) return false;
213
- for (let i = 1; i < nums.length; i++) if (!(nums[i] > nums[i - 1])) return false;
214
- return true;
360
+ // ---------------------------------------------------------------------------
361
+ // Validation — SCHEMA.md rules 1–12 (rule 12 is enforced at emit time)
362
+ // ---------------------------------------------------------------------------
363
+
364
+ // True when a token's $value is absent or vacuously empty.
365
+ function isEmptyValue(v) {
366
+ return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
215
367
  }
216
368
 
217
- // Run the coverage gate; return { errors: string[] }.
218
- function runCoverageGate(doc) {
219
- const errors = [];
220
- const { frontmatter, primitives, manifest } = doc;
221
-
222
- // Rule 1 — frontmatter with schema and legal provenance.
223
- if (!frontmatter) {
224
- errors.push("frontmatter: missing --- frontmatter block");
225
- } else {
226
- if (frontmatter.schema !== "design-md/v1") {
227
- errors.push(`frontmatter: schema must be "design-md/v1" (found ${frontmatter.schema ?? "nothing"})`);
369
+ // Verify a preserved-css color token: parse-back equality plus the srgb hex round-trip.
370
+ function verifyColorParseBack(name, token, errors) {
371
+ const css = token.$extensions?.[EXT]?.css;
372
+ if (typeof css !== "string" || isAlias(token.$value)) return;
373
+ let reparsed;
374
+ try { reparsed = parseColorCss(css); } catch (e) { errors.push(`rule 9 — ${name}: ${e.message}`); return; }
375
+ if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) {
376
+ errors.push(`rule 9 — ${name}: parse-back mismatch — css ${JSON.stringify(css)} → ${JSON.stringify(reparsed)} != $value ${JSON.stringify(token.$value)}`);
377
+ return;
378
+ }
379
+ if (token.$value.colorSpace === "srgb") {
380
+ const viaHex = parseColorCss(token.$value.hex);
381
+ if (JSON.stringify(viaHex.components) !== JSON.stringify(token.$value.components)) {
382
+ errors.push(`rule 9 — ${name}: srgb components do not round-trip through hex ${token.$value.hex}`);
228
383
  }
229
- if (!LEGAL_PROVENANCE.includes(frontmatter.provenance)) {
230
- errors.push(`frontmatter: provenance must be one of ${LEGAL_PROVENANCE.join("|")} (found ${frontmatter.provenance ?? "nothing"})`);
384
+ }
385
+ }
386
+
387
+ // Rule 9 for one non-color token carrying a preserved string .css: parsing it must reproduce $value.
388
+ function verifyScalarParseBack(name, token, css, errors) {
389
+ if (isAlias(token.$value)) return; // an aliased token takes the target's .css; rule 6 owns alias integrity
390
+ try {
391
+ if (token.$type === "cubicBezier") {
392
+ if (JSON.stringify(parseCubicBezierCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
393
+ } else if (token.$type === "fontFamily") {
394
+ if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
395
+ } else if (token.$type === "number") {
396
+ if (parseTrackingCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
397
+ } else if (token.$type === "dimension") {
398
+ if (JSON.stringify(parseDimensionCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
399
+ } else if (token.$type === "duration") {
400
+ if (JSON.stringify(parseDurationCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
401
+ } else if (token.$type === "fontWeight") {
402
+ if (parseNumberCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
231
403
  }
404
+ } catch (e) {
405
+ errors.push(`rule 9 — ${name}: ${e.message}`);
232
406
  }
407
+ }
233
408
 
234
- // Rule 2 — both machine blocks present and parseable.
235
- if (!primitives) errors.push("block: missing or unparseable `yaml design-primitives` block");
236
- if (!manifest) errors.push("block: missing or unparseable `yaml lever-manifest` block");
237
- if (!primitives || !manifest) return { errors };
409
+ // Run the full coverage gate (rules 1–11) over one artifact directory; returns everything emit needs.
410
+ function runCheck(dir, theme) {
411
+ const errors = [];
412
+ const artifact = loadArtifact(dir, errors);
413
+ if (!artifact) return { errors };
414
+ const { base, files, views, payload } = artifact;
238
415
 
239
- // Rule 3 — every primitive assigned or hinted.
240
- for (const key of PRIMITIVES) {
241
- const v = primitives[key];
242
- if (v === undefined) errors.push(`missing primitive: ${key}`);
243
- else if (!isFilled(v)) errors.push(`empty primitive: ${key}`);
416
+ if (!views.has(theme)) {
417
+ errors.push(`rule 1 — resolver: --theme ${theme} requested but the resolver carries no ${theme} context`);
418
+ return { errors };
244
419
  }
245
420
 
246
- // Rule 4 — dials, tier, and traits.
247
- if (!LEGAL_VERBOSITY.includes(primitives.verbosity)) {
248
- errors.push(`dial verbosity: must be one of ${LEGAL_VERBOSITY.join("|")} (found ${primitives.verbosity ?? "nothing"})`);
421
+ // Rule 2 — the file-level payload on the base root.
422
+ if (!payload || typeof payload !== "object") {
423
+ errors.push(`rule 2 — payload: missing $extensions["${EXT}"] on the base file's root group`);
424
+ return { errors };
249
425
  }
250
- if (!LEGAL_FEEDBACK.includes(primitives.feedback)) {
251
- errors.push(`dial feedback: must be one of ${LEGAL_FEEDBACK.join("|")} (found ${primitives.feedback ?? "nothing"})`);
426
+ if (payload.schema !== "design-tokens/v1") errors.push(`rule 2 — payload.schema: must be "design-tokens/v1" (found ${JSON.stringify(payload.schema)})`);
427
+ if (payload.dtcg !== "2025.10") errors.push(`rule 2 — payload.dtcg: must be "2025.10" (found ${JSON.stringify(payload.dtcg)})`);
428
+ if (typeof payload.product !== "string" || payload.product.trim() === "") errors.push("rule 2 — payload.product: missing or empty");
429
+ if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`rule 2 — payload.provenance: must be one of ${LEGAL_PROVENANCE.join("|")} (found ${JSON.stringify(payload.provenance)})`);
430
+ 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)})`);
431
+ if (!LEGAL_VERBOSITY.includes(payload.verbosity)) errors.push(`rule 2 — payload.verbosity: must be one of ${LEGAL_VERBOSITY.join("|")} (found ${JSON.stringify(payload.verbosity)})`);
432
+ if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`rule 2 — payload.feedback: must be one of ${LEGAL_FEEDBACK.join("|")} (found ${JSON.stringify(payload.feedback)})`);
433
+ 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"])})`);
434
+ if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("rule 2 — payload.product-traits: must be a non-empty array");
435
+ const lm = payload["lever-manifest"];
436
+ for (const key of MANIFEST_KEYS) {
437
+ if (lm?.[key] === undefined) errors.push(`rule 2 — payload.lever-manifest.${key}: missing`);
252
438
  }
253
- if (!LEGAL_DENSITY.includes(primitives["density-tier"])) {
254
- errors.push(`density-tier: must be one of ${LEGAL_DENSITY.join("|")} (found ${primitives["density-tier"] ?? "nothing"})`);
439
+
440
+ const lightView = views.get("light");
441
+ const lightTokens = collectTokens(lightView);
442
+
443
+ // Rule 3 — all 26 primitives present with the exact $type, each assigned or hinted.
444
+ for (const [key, type] of PRIMITIVE_TYPES) {
445
+ const t = lightView.primitive?.[key];
446
+ if (!isToken(t)) { errors.push(`rule 3 — primitive.${key}: missing`); continue; }
447
+ if (t.$type !== type) errors.push(`rule 3 — primitive.${key}: $type ${JSON.stringify(t.$type)} (want ${type})`);
448
+ if (isEmptyValue(t.$value)) errors.push(`rule 3 — primitive.${key}: empty $value`);
255
449
  }
256
- const traits = primitives["product-traits"];
257
- if (!Array.isArray(traits) || traits.length === 0) {
258
- errors.push("product-traits: must be a non-empty list");
450
+
451
+ // Rule 4 — all 34 color roles present, typed color, legal colorSpace, .css present (via alias when aliased).
452
+ for (const role of ALL_TOKENS) {
453
+ const t = lightView.color?.[role];
454
+ if (!isToken(t)) { errors.push(`rule 4 — color.${role}: missing`); continue; }
455
+ if (t.$type !== "color") { errors.push(`rule 4 — color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
456
+ const target = isAlias(t.$value) ? resolveAlias(t.$value, lightTokens, "color", `color.${role}`, []) : t;
457
+ if (!target) continue; // dangling alias — rule 6 reports it
458
+ if (!["oklch", "srgb"].includes(target.$value?.colorSpace)) errors.push(`rule 4 — color.${role}: colorSpace ${JSON.stringify(target.$value?.colorSpace)} (want oklch|srgb)`);
459
+ if (typeof target.$extensions?.[EXT]?.css !== "string") errors.push(`rule 4 — color.${role}: missing preserved .css`);
259
460
  }
260
461
 
261
- // Rule 5 — every color token valued under color-tokens.light.
262
- const light = manifest["color-tokens"] && manifest["color-tokens"].light;
263
- if (!light || typeof light !== "object") {
264
- errors.push("color-tokens.light: missing color-tokens.light map");
265
- } else {
266
- for (const token of ALL_TOKENS) {
267
- const v = light[token];
268
- if (v === undefined) errors.push(`missing color token: ${token}`);
269
- else if (!isFilled(v)) errors.push(`empty color token: ${token}`);
462
+ // Rule 5 — typography coverage: the three families and the six-step ramp with all five sub-values.
463
+ for (const fam of ["heading", "body", "mono"]) {
464
+ const t = lightView.typography?.family?.[fam];
465
+ if (!isToken(t) || t.$type !== "fontFamily") errors.push(`rule 5 — typography.family.${fam}: missing or not $type fontFamily`);
466
+ }
467
+ for (const step of RAMP_STEPS) {
468
+ const t = lightView.typography?.ramp?.[step];
469
+ if (!isToken(t) || t.$type !== "typography") { errors.push(`rule 5 — typography.ramp.${step}: missing or not $type typography`); continue; }
470
+ for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
471
+ if (t.$value?.[sub] === undefined) errors.push(`rule 5 — typography.ramp.${step}.${sub}: missing sub-value`);
270
472
  }
271
473
  }
272
474
 
273
- // Rule 6 — scales, motion, fonts, icons, accent-count.
274
- const scaleCheck = (name, arr, allowAscending = true) => {
275
- if (!Array.isArray(arr) || arr.length === 0) errors.push(`${name}: must be a non-empty list`);
276
- else if (allowAscending && !isAscending(arr)) errors.push(`${name}: values must be ascending (found ${arr.join(", ")})`);
277
- };
278
- scaleCheck("spacing-scale", manifest["spacing-scale"]);
279
- scaleCheck("radius-scale", manifest["radius-scale"]);
280
-
281
- const shadow = manifest["shadow-levels"];
282
- if (!Array.isArray(shadow) || shadow.length === 0) {
283
- errors.push("shadow-levels: must be a non-empty list");
284
- } else {
285
- const ranks = shadow.map((s) => SHADOW_RANK[String(s).trim()]);
286
- if (ranks.every((r) => r !== undefined)) {
287
- for (let i = 1; i < ranks.length; i++) {
288
- if (!(ranks[i] > ranks[i - 1])) { errors.push(`shadow-levels: must be ordered by increasing intensity (found ${shadow.join(", ")})`); break; }
475
+ // Rule 6 — alias integrity over every resolved context view (aliases resolve after overlay).
476
+ const SUB_TYPES = { fontFamily: "fontFamily", fontWeight: "fontWeight", letterSpacing: "number", fontSize: "dimension", lineHeight: "number" };
477
+ for (const [context, view] of views) {
478
+ const tokens = collectTokens(view);
479
+ const label = context === "light" ? "" : ` [${context}]`;
480
+ for (const [path, token] of tokens) {
481
+ if (isAlias(token.$value)) resolveAlias(token.$value, tokens, token.$type, `${path}${label}`, errors);
482
+ if (token.$type === "typography" && token.$value && typeof token.$value === "object") {
483
+ for (const [sub, want] of Object.entries(SUB_TYPES)) {
484
+ const v = token.$value[sub];
485
+ if (isAlias(v)) resolveAlias(v, tokens, want, `${path}.${sub}${label}`, errors);
486
+ }
289
487
  }
290
488
  }
291
489
  }
292
490
 
293
- const motion = manifest.motion;
294
- if (!motion || typeof motion !== "object") {
295
- errors.push("motion: missing motion block");
296
- } else {
297
- scaleCheck("motion.durations", motion.durations);
298
- if (!Array.isArray(motion.easings) || motion.easings.length === 0) errors.push("motion.easings: must be a non-empty list");
299
- }
491
+ // Rule 7 — manifest scales.
492
+ if (lm && typeof lm === "object") {
493
+ const ascending = (arr) => arr.every((v, i) => i === 0 || v > arr[i - 1]);
494
+ for (const key of ["spacing-scale", "radius-scale"]) {
495
+ 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])})`);
496
+ }
497
+ 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)})`);
498
+ if (!Array.isArray(lm.motion?.easings) || lm.motion.easings.length === 0) errors.push("rule 7 — lever-manifest.motion.easings: must be non-empty");
499
+ const shadows = lm["shadow-levels"];
500
+ const ranks = (Array.isArray(shadows) ? shadows : []).map((s) => SHADOW_RANK[s]);
501
+ if (ranks.length === 0 || ranks.some((r) => r === undefined) || !ascending(ranks)) {
502
+ 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)})`);
503
+ }
504
+ if (typeof lm.icons?.family !== "string" || lm.icons.family === "") errors.push("rule 7 — lever-manifest.icons.family: missing or empty");
505
+ if (!(lm["accent-count"] >= 1)) errors.push(`rule 7 — lever-manifest.accent-count: must be >= 1 (found ${JSON.stringify(lm?.["accent-count"])})`);
300
506
 
301
- const fonts = manifest.fonts;
302
- if (!fonts || typeof fonts !== "object") {
303
- errors.push("fonts: missing fonts block");
304
- } else {
305
- if (!isFilled(fonts.sans)) errors.push("fonts.sans: missing or empty");
306
- if (!isFilled(fonts.mono)) errors.push("fonts.mono: missing or empty");
507
+ // Rule 8 — the look's own base values sit on its own allowlist.
508
+ const radius = lightView.primitive?.radius?.$value?.value;
509
+ if (Array.isArray(lm["radius-scale"]) && radius !== undefined && !lm["radius-scale"].includes(radius)) {
510
+ errors.push(`rule 8 — primitive.radius: ${radius} not on lever-manifest.radius-scale`);
511
+ }
512
+ for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
513
+ const ms = lightView.primitive?.[key]?.$value?.value;
514
+ if (Array.isArray(lm.motion?.durations) && ms !== undefined && !lm.motion.durations.includes(ms)) {
515
+ errors.push(`rule 8 — primitive.${key}: ${ms} not in lever-manifest.motion.durations`);
516
+ }
517
+ }
518
+ const ease = lightView.primitive?.["ease-default"];
519
+ if (isToken(ease) && Array.isArray(lm.motion?.easings)) {
520
+ const easeCss = typeof ease.$extensions?.[EXT]?.css === "string" ? ease.$extensions[EXT].css : serializeValue("cubicBezier", ease.$value);
521
+ if (!lm.motion.easings.includes(easeCss)) errors.push(`rule 8 — primitive.ease-default: ${JSON.stringify(easeCss)} not in lever-manifest.motion.easings`);
522
+ }
307
523
  }
308
524
 
309
- const icons = manifest.icons;
310
- if (!icons || typeof icons !== "object") {
311
- errors.push("icons: missing icons block");
312
- } else {
313
- if (!isFilled(icons.family)) errors.push("icons.family: missing or empty");
314
- if (icons.stroke === undefined || String(icons.stroke).trim() === "") errors.push("icons.stroke: missing or empty");
525
+ // Rule 9 — parse-back for every token carrying a preserved .css, per file.
526
+ for (const [fileName, doc] of files) {
527
+ const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
528
+ for (const [path, token] of collectTokens(doc)) {
529
+ const css = token.$extensions?.[EXT]?.css;
530
+ if (css === undefined) continue;
531
+ const name = `${prefix}${path}`;
532
+ if (token.$type === "color") {
533
+ verifyColorParseBack(name, token, errors);
534
+ } else if (typeof css === "string") {
535
+ verifyScalarParseBack(name, token, css, errors);
536
+ } else if (token.$type === "typography" && typeof css === "object" && css !== null) {
537
+ if (typeof css.letterSpacing === "string" && typeof token.$value?.letterSpacing === "number" && typeof token.$value?.fontSize?.value === "number") {
538
+ try {
539
+ const reparsed = parseLetterSpacingCss(css.letterSpacing, token.$value.fontSize.value);
540
+ if (reparsed !== token.$value.letterSpacing) errors.push(`rule 9 — ${name}: letterSpacing css ${css.letterSpacing} → ${reparsed} != ${token.$value.letterSpacing}`);
541
+ } catch (e) {
542
+ errors.push(`rule 9 — ${name}: ${e.message}`);
543
+ }
544
+ }
545
+ }
546
+ }
315
547
  }
316
548
 
317
- const accent = parseFloat(manifest["accent-count"]);
318
- if (Number.isNaN(accent) || accent < 1) errors.push(`accent-count: must be >= 1 (found ${manifest["accent-count"] ?? "nothing"})`);
549
+ // Rule 10 — the dark file, when present, carries exactly the 34-role color group and nothing else.
550
+ const darkDoc = files.get("design.tokens.dark.json");
551
+ if (darkDoc !== undefined) {
552
+ const rootKeys = Object.keys(darkDoc).filter((k) => !k.startsWith("$"));
553
+ if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`rule 10 — design.tokens.dark.json: root groups ${JSON.stringify(rootKeys)} (want exactly ["color"])`);
554
+ 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)");
555
+ for (const role of ALL_TOKENS) {
556
+ const t = darkDoc.color?.[role];
557
+ if (!isToken(t)) { errors.push(`rule 10 — dark color.${role}: missing`); continue; }
558
+ if (t.$type !== "color") { errors.push(`rule 10 — dark color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
559
+ if (isAlias(t.$value)) continue; // resolves post-overlay; rules 4/6 cover it on the dark view
560
+ if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`rule 10 — dark color.${role}: colorSpace ${JSON.stringify(t.$value?.colorSpace)} (want oklch|srgb)`);
561
+ if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 10 — dark color.${role}: missing preserved .css`);
562
+ }
563
+ for (const role of Object.keys(darkDoc.color ?? {})) {
564
+ if (!ALL_TOKENS.includes(role)) errors.push(`rule 10 — dark color.${role}: not a legal role`);
565
+ }
566
+ }
319
567
 
320
- return { errors };
321
- }
568
+ // Rule 11 — only the eight legal $type values appear, in any file.
569
+ for (const [fileName, doc] of files) {
570
+ const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
571
+ for (const [path, token] of collectTokens(doc)) {
572
+ if (!LEGAL_TYPES.includes(token.$type)) errors.push(`rule 11 — ${prefix}${path}: illegal $type ${JSON.stringify(token.$type)}`);
573
+ }
574
+ }
322
575
 
323
- // Collect every bare "hint:" leaf across primitives, dials, and color tokens for the emit refusal.
324
- function collectBareHints(doc) {
576
+ // Rule 12 inventory — every token still carrying a hint (passes --check; --emit refuses).
325
577
  const hints = [];
326
- const { primitives, manifest } = doc;
327
- if (primitives) {
328
- for (const key of [...PRIMITIVES, "verbosity", "feedback", "density-tier"]) {
329
- if (isHint(primitives[key])) hints.push({ key, text: primitives[key].trim().slice(5).trim() });
578
+ for (const [fileName, doc] of files) {
579
+ const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
580
+ for (const [path, token] of collectTokens(doc)) {
581
+ const hint = token.$extensions?.[EXT]?.hint;
582
+ if (hint !== undefined) hints.push({ key: `${prefix}${path}`, text: String(hint) });
330
583
  }
331
584
  }
332
- const light = manifest && manifest["color-tokens"] && manifest["color-tokens"].light;
333
- if (light) {
334
- for (const token of ALL_TOKENS) {
335
- if (isHint(light[token])) hints.push({ key: `color-tokens.light.${token}`, text: light[token].trim().slice(5).trim() });
336
- }
585
+
586
+ return { errors, payload, views, hints, hasDark: files.has("design.tokens.dark.json") };
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // Emitter feed — flatten the resolved views into the value map the emitters consume
591
+ // ---------------------------------------------------------------------------
592
+
593
+ // Assemble the emitters' document: 26 primitive CSS strings + the payload dials, and the
594
+ // 34-role CSS string map per theme (preferring each token's preserved .css).
595
+ function buildEmitterDoc(views, payload, errors) {
596
+ const lightTokens = collectTokens(views.get("light"));
597
+ const primitives = {};
598
+ for (const key of PRIMITIVES) primitives[key] = cssFor(`primitive.${key}`, lightTokens, errors);
599
+ primitives.verbosity = payload.verbosity;
600
+ primitives.feedback = payload.feedback;
601
+ primitives["density-tier"] = payload["density-tier"];
602
+
603
+ const colorTokens = { light: {} };
604
+ for (const role of ALL_TOKENS) colorTokens.light[role] = cssFor(`color.${role}`, lightTokens, errors);
605
+
606
+ if (views.has("dark")) {
607
+ const darkTokens = collectTokens(views.get("dark"));
608
+ colorTokens.dark = {};
609
+ for (const role of ALL_TOKENS) colorTokens.dark[role] = cssFor(`color.${role}`, darkTokens, errors);
337
610
  }
338
- return hints;
611
+
612
+ return { primitives, manifest: { "color-tokens": colorTokens } };
339
613
  }
340
614
 
615
+ // ---------------------------------------------------------------------------
616
+ // Emitters (values come from the token artifact; non-comment output is v1 byte-identical)
617
+ // ---------------------------------------------------------------------------
618
+
341
619
  // Derive a rosterless data-style slug from the product name.
342
620
  function slugify(name) {
343
621
  return String(name || "project").toLowerCase().trim()
@@ -388,28 +666,48 @@ function buildStyleBlockLines(primitives) {
388
666
  return lines.join("\n");
389
667
  }
390
668
 
391
- // Emit the full src/styles/personality.css content for a passing document.
392
- function emitPersonalityCss(doc, slug) {
669
+ // Emit the full src/styles/personality.css content for a passing artifact.
670
+ function emitPersonalityCss(doc, slug, meta) {
393
671
  const p = doc.primitives;
394
672
  const light = doc.manifest["color-tokens"].light;
395
673
  const density = p["density-tier"];
396
674
  const verbosity = p.verbosity;
397
675
  const feedback = p.feedback;
398
676
 
677
+ const dark = doc.manifest["color-tokens"].dark;
678
+
399
679
  const semanticLines = SEMANTIC_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
400
680
  const substrateLines = SUBSTRATE_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
401
681
  const styleLines = buildStyleBlockLines(p);
402
682
 
683
+ const darkSection = dark
684
+ ? `/* ── Dark theme — OPT-IN ONLY (this look ships a dark overlay). ──
685
+ * design.tokens.dark.json remaps all 34 color tokens; <html data-theme="dark">
686
+ * opts in. Light stays the default. */
687
+ [data-theme="dark"] {
688
+ /* semantic (17) */
689
+ ${SEMANTIC_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
690
+ /* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
691
+ ${SUBSTRATE_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
692
+ }`
693
+ : `/* ── Dark theme — OPT-IN ONLY. ──
694
+ * Emitted only when the artifact ships a design.tokens.dark.json dark overlay.
695
+ * Light stays the default; this block never appears for a light-only project.
696
+ [data-theme="dark"] {
697
+ --background: ...; --foreground: ...; ... all 34 tokens remapped ...
698
+ } */`;
699
+
403
700
  return `/* src/styles/personality.css — operator-UI personality tokens (emitted; do not hand-edit).
404
701
  *
405
- * Active personality — code is the cache. There is no JSON sidecar; these values
406
- * and the <html data-*> attributes in index.html ARE the state. To change the look,
407
- * edit the resolved design.md and re-run derive-personality.mjs --emit.
702
+ * Active personality — code is the cache. These values and the <html data-*>
703
+ * attributes in index.html are emitted from the design-tokens artifact. To change
704
+ * the look, edit design.tokens.json and re-run derive-personality.mjs --emit.
408
705
  * style: ${slug} (<html data-style="${slug}">) — the project's own personality (rosterless)
409
706
  * density: ${density} (<html data-density="${density}">)
410
707
  * verbosity: ${verbosity} (<html data-verbosity="${verbosity}">)
411
708
  * feedback: ${feedback} (<html data-feedback="${feedback}">)
412
- * theme: light (<html data-theme="light">) — light is the default; dark is opt-in
709
+ * theme: ${meta.theme} (<html data-theme="${meta.theme}">) — light is the default; dark is opt-in
710
+ * artifact: design.tokens.json (resolved via design.resolver.json) — provenance: ${meta.provenance}, sourced ${meta.sourced}
413
711
  *
414
712
  * Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
415
713
  * this file resolves those names to values. Components never use dark: prefixes or literal colors.
@@ -480,8 +778,8 @@ ${substrateLines}
480
778
  }
481
779
 
482
780
  /* ── The project personality: the single [data-style] block (D3, rosterless). ──
483
- * Named after the project — not a preset. This materializes the design.md
484
- * design-primitives block: all 26 primitives + the derived temporal. It is the
781
+ * Named after the project — not a preset. This materializes the artifact's
782
+ * primitive token group: all 26 primitives + the derived temporal. It is the
485
783
  * one block improve/extract rewrites. density-gap/inset are governed refs, so
486
784
  * the global tier and per-surface step still compose. */
487
785
  [data-style="${slug}"] {
@@ -508,16 +806,11 @@ ${styleLines}
508
806
  * A surface element carries data-density-step; it re-derives one step off the
509
807
  * inherited base. Reads --density-base-* (never itself) so it composes with the
510
808
  * tier and never absolutely overrides it. The recomputed --density-gap flows into
511
- * the library's p-*/gap-* through the @theme --spacing remap. */
809
+ * the library's p-* and gap-* through the @theme --spacing remap. */
512
810
  [data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
513
811
  [data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
514
812
 
515
- /* ── Dark theme — OPT-IN ONLY. ──
516
- * Emitted only when the design.md ships a lever-manifest color-tokens.dark map.
517
- * Light stays the default; this block never appears for a light-only project.
518
- [data-theme="dark"] {
519
- --background: ...; --foreground: ...; ... all 34 tokens remapped ...
520
- } */
813
+ ${darkSection}
521
814
 
522
815
  /* ── Accessibility ── */
523
816
  @media (prefers-reduced-motion: reduce) {
@@ -548,7 +841,7 @@ function emitGlobalsCss(doc) {
548
841
  /* CASCADE CONTRACT 2: this four-step order is LOAD-BEARING. The library's
549
842
  * styles.css ships its own :root token defaults and its own @theme radius scale;
550
843
  * our @theme MUST come AFTER the import to win the radius scale and own every
551
- * mapping. rules/general.md (frontend section) states the same order — co-authority. */
844
+ * mapping. rules/frontend.md states the same order — co-authority. */
552
845
  @import "./personality.css"; /* 1. token source (must be first) */
553
846
  @import "tailwindcss"; /* 2. framework */
554
847
  @import "@nurix/components/styles.css"; /* 3. library (before our @theme) */
@@ -561,9 +854,9 @@ function emitGlobalsCss(doc) {
561
854
  --radius-lg: var(--radius);
562
855
  --radius-xl: calc(var(--radius) + 4px);
563
856
 
564
- /* density remap — the single line that scales ALL library spacing. Every p-*/gap-*
565
- * is calc(var(--spacing) × N); driving --spacing off --density-gap flows the tier
566
- * AND the per-surface density step into the library's internals. */
857
+ /* density remap — the single line that scales ALL library spacing. Every p-* and
858
+ * gap-* is calc(var(--spacing) x N); driving --spacing off --density-gap flows the
859
+ * tier AND the per-surface density step into the library's internals. */
567
860
  --spacing: calc(0.25rem * var(--density-gap));
568
861
 
569
862
  /* shadow — alpha rides --shadow-intensity (matches the library's shadow-xs/sm/md/lg use) */
@@ -611,18 +904,18 @@ ${colorMaps}
611
904
  }
612
905
 
613
906
  // Emit the index.html <html> attribute snippet the shell must carry.
614
- function emitIndexHtmlAttributes(doc, slug) {
907
+ function emitIndexHtmlAttributes(doc, slug, meta) {
615
908
  const p = doc.primitives;
616
909
  return `<!-- Personality state lives in these <html data-*> attributes — code is the cache.
617
- There is no sidecar JSON. To change the look, edit the resolved design.md and
618
- re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
910
+ The design-tokens artifact is the source. To change the look, edit design.tokens.json
911
+ and re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
619
912
  <html
620
913
  lang="en"
621
914
  data-style="${slug}"
622
915
  data-density="${p["density-tier"]}"
623
916
  data-verbosity="${p.verbosity}"
624
917
  data-feedback="${p.feedback}"
625
- data-theme="light"
918
+ data-theme="${meta.theme}"
626
919
  >
627
920
  `;
628
921
  }
@@ -633,64 +926,71 @@ function writeFileEnsured(path, content) {
633
926
  writeFileSync(path, content);
634
927
  }
635
928
 
929
+ // ---------------------------------------------------------------------------
930
+ // CLI
931
+ // ---------------------------------------------------------------------------
932
+
636
933
  // Print usage and exit non-zero.
637
934
  function usage() {
638
- process.stderr.write("usage: derive-personality.mjs <design.md> [--check | --emit <outdir>]\n");
935
+ process.stderr.write("usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir>] [--theme light|dark]\n");
639
936
  process.exit(2);
640
937
  }
641
938
 
642
- // Entry point: parse argv, run --check or --emit.
939
+ // Entry point: parse argv, run --check or --emit over the artifact directory.
643
940
  function main() {
644
941
  const argv = process.argv.slice(2);
645
- let designPath = null;
942
+ let artifactDir = null;
646
943
  let mode = "check";
647
944
  let outdir = null;
945
+ let theme = "light";
648
946
  for (let i = 0; i < argv.length; i++) {
649
947
  const a = argv[i];
650
948
  if (a === "--check") mode = "check";
651
949
  else if (a === "--emit") { mode = "emit"; outdir = argv[++i] ?? null; }
652
- else if (!a.startsWith("-") && designPath === null) designPath = a;
950
+ else if (a === "--theme") { theme = argv[++i] ?? null; }
951
+ else if (!a.startsWith("-") && artifactDir === null) artifactDir = a;
653
952
  else usage();
654
953
  }
655
- if (!designPath) usage();
954
+ if (!artifactDir) usage();
656
955
  if (mode === "emit" && !outdir) usage();
956
+ if (!LEGAL_THEMES.includes(theme)) usage();
657
957
 
658
- let source;
659
- try {
660
- source = readFileSync(designPath, "utf8");
661
- } catch {
662
- process.stderr.write(`error: cannot read ${designPath}\n`);
663
- process.exit(2);
664
- }
665
-
666
- const doc = parseDesignMd(source);
667
- const { errors } = runCoverageGate(doc);
668
- const slug = slugify(doc.frontmatter && doc.frontmatter.product);
958
+ const { errors, payload, views, hints, hasDark } = runCheck(artifactDir, theme);
669
959
 
670
960
  if (errors.length > 0) {
671
- process.stderr.write(`coverage gate FAILED (${designPath}): ${errors.length} issue(s)\n`);
961
+ process.stderr.write(`check FAILED (${artifactDir}): ${errors.length} issue(s)\n`);
672
962
  for (const e of errors) process.stderr.write(` - ${e}\n`);
673
963
  process.exit(1);
674
964
  }
675
965
 
966
+ const slug = slugify(payload.product);
967
+
676
968
  if (mode === "check") {
677
- const provenance = doc.frontmatter.provenance;
678
- process.stdout.write(`coverage gate PASSED (${designPath}): 26 primitives, verbosity/feedback/density-tier, 34 color tokens, scales OK — provenance=${provenance}, style=${slug}\n`);
969
+ const hintNote = hints.length > 0 ? `, ${hints.length} hint(s) outstanding (emit will refuse)` : "";
970
+ process.stdout.write(`check PASSED (${artifactDir}): rules 1–11 OK — 26 primitives, 34 color roles, typography 3+6, payload/manifest/parse-back OK — provenance=${payload.provenance}, style=${slug}, theme=${theme}${hasDark ? ", dark overlay present" : ""}${hintNote}\n`);
679
971
  process.exit(0);
680
972
  }
681
973
 
682
- // --emit: refuse while any bare hint remains.
683
- const hints = collectBareHints(doc);
974
+ // Rule 12 — emission precondition: refuse while any token carries a hint.
684
975
  if (hints.length > 0) {
685
- process.stderr.write(`emit REFUSED (${designPath}): ${hints.length} unresolved hint(s) — resolve each to a concrete value before emission.\n`);
976
+ process.stderr.write(`emit REFUSED (${artifactDir}): ${hints.length} unresolved hint(s) — rule 12: every gated token must be concrete before emission.\n`);
686
977
  for (const h of hints) process.stderr.write(` - ${h.key}: hint: ${h.text}\n`);
687
- process.stderr.write(" Resolve with judgment and record the rationale: \"<key>: <value> # derived: <rationale>\", then re-run --emit.\n");
978
+ 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");
979
+ process.exit(1);
980
+ }
981
+
982
+ const emitErrors = [];
983
+ const doc = buildEmitterDoc(views, payload, emitErrors);
984
+ if (emitErrors.length > 0) {
985
+ process.stderr.write(`emit FAILED (${artifactDir}): ${emitErrors.length} issue(s)\n`);
986
+ for (const e of emitErrors) process.stderr.write(` - ${e}\n`);
688
987
  process.exit(1);
689
988
  }
690
989
 
691
- const personalityCss = emitPersonalityCss(doc, slug);
990
+ const meta = { provenance: payload.provenance, sourced: payload.sourced, theme };
991
+ const personalityCss = emitPersonalityCss(doc, slug, meta);
692
992
  const globalsCss = emitGlobalsCss(doc);
693
- const htmlAttrs = emitIndexHtmlAttributes(doc, slug);
993
+ const htmlAttrs = emitIndexHtmlAttributes(doc, slug, meta);
694
994
  const personalityPath = join(outdir, "src/styles/personality.css");
695
995
  const globalsPath = join(outdir, "src/styles/globals.css");
696
996
  const attrsPath = join(outdir, "index-html-attributes.txt");