@nurix/etna 0.19.11 → 0.19.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (486) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +1 -1
  3. package/data/harnesses/code/rules/frontend.md +5 -5
  4. package/data/harnesses/code/skills/design/SKILL.md +70 -50
  5. package/data/harnesses/code/skills/design/components/charts.md +76 -0
  6. package/data/harnesses/code/skills/design/components/collections.md +132 -0
  7. package/data/harnesses/code/skills/design/components/feedback.md +47 -0
  8. package/data/harnesses/code/skills/design/components/hooks.md +35 -0
  9. package/data/harnesses/code/skills/design/components/index.md +143 -0
  10. package/data/harnesses/code/skills/design/components/inputs.md +327 -0
  11. package/data/harnesses/code/skills/design/components/layout.md +52 -0
  12. package/data/harnesses/code/skills/design/components/media.md +66 -0
  13. package/data/harnesses/code/skills/design/components/motion.md +82 -0
  14. package/data/harnesses/code/skills/design/components/navigation.md +29 -0
  15. package/data/harnesses/code/skills/design/components/overlays.md +72 -0
  16. package/data/harnesses/code/skills/design/components/setup.md +40 -0
  17. package/data/harnesses/code/skills/design/components/typography.md +46 -0
  18. package/data/harnesses/code/skills/design/components/utils.md +37 -0
  19. package/data/harnesses/code/skills/design/data/blueprints/browse.md +38 -0
  20. package/data/harnesses/code/skills/design/data/blueprints/creation-flow.md +38 -0
  21. package/data/harnesses/code/skills/design/data/blueprints/dashboard.md +38 -0
  22. package/data/harnesses/code/skills/design/data/blueprints/grid.md +38 -0
  23. package/data/harnesses/code/skills/design/data/blueprints/queue.md +38 -0
  24. package/data/harnesses/code/skills/design/data/blueprints/record-detail.md +38 -0
  25. package/data/harnesses/code/skills/design/data/blueprints/settings.md +38 -0
  26. package/data/harnesses/code/skills/design/data/chart-styling/Readme.md +33 -0
  27. package/data/harnesses/code/skills/design/data/chart-styling/carbon-accessible-alert-semantic.md +19 -0
  28. package/data/harnesses/code/skills/design/data/chart-styling/carbon-monochrome-sequential-trend.md +19 -0
  29. package/data/harnesses/code/skills/design/data/chart-styling/datadog-classic-six-aligned.md +19 -0
  30. package/data/harnesses/code/skills/design/data/chart-styling/datawrapper-tiered-gray-hierarchy.md +19 -0
  31. package/data/harnesses/code/skills/design/data/chart-styling/econ-red-rule-horizontal.md +19 -0
  32. package/data/harnesses/code/skills/design/data/chart-styling/financial-diverging-threshold-overlay.md +19 -0
  33. package/data/harnesses/code/skills/design/data/chart-styling/ft-vocabulary-ranked-muted.md +19 -0
  34. package/data/harnesses/code/skills/design/data/chart-styling/grafana-dark-threshold-bands.md +19 -0
  35. package/data/harnesses/code/skills/design/data/chart-styling/material-rounded-explicit-label.md +19 -0
  36. package/data/harnesses/code/skills/design/data/chart-styling/nyt-ghost-grid-direct-label.md +19 -0
  37. package/data/harnesses/code/skills/design/data/chart-styling/plot-currentcolor-inherit.md +19 -0
  38. package/data/harnesses/code/skills/design/data/chart-styling/saas-gradient-glow-area.md +19 -0
  39. package/data/harnesses/code/skills/design/data/chart-styling/shadcn-composable-css-vars.md +19 -0
  40. package/data/harnesses/code/skills/design/data/charts/census.csv +58 -0
  41. package/data/harnesses/code/skills/design/data/checklists/browse.md +21 -0
  42. package/data/harnesses/code/skills/design/data/checklists/creation-flow.md +20 -0
  43. package/data/harnesses/code/skills/design/data/checklists/dashboard.md +21 -0
  44. package/data/harnesses/code/skills/design/data/checklists/grid.md +21 -0
  45. package/data/harnesses/code/skills/design/data/checklists/queue.md +21 -0
  46. package/data/harnesses/code/skills/design/data/checklists/record-detail.md +21 -0
  47. package/data/harnesses/code/skills/design/data/checklists/settings.md +21 -0
  48. package/data/harnesses/code/skills/design/data/chrome/board-column.md +68 -0
  49. package/data/harnesses/code/skills/design/data/chrome/data-table.md +63 -0
  50. package/data/harnesses/code/skills/design/data/chrome/side-panel.md +60 -0
  51. package/data/harnesses/code/skills/design/data/chrome/sidebar.md +78 -0
  52. package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +62 -0
  53. package/data/harnesses/code/skills/design/data/chrome/topbar.md +65 -0
  54. package/data/harnesses/code/skills/design/data/color/accent-presets.md +90 -0
  55. package/data/harnesses/code/skills/design/data/color/aesop-earth-mute.md +91 -0
  56. package/data/harnesses/code/skills/design/data/color/arc-peach-play.md +90 -0
  57. package/data/harnesses/code/skills/design/data/color/attio-hunter-teal.md +90 -0
  58. package/data/harnesses/code/skills/design/data/color/carbon-cyan-signal.md +90 -0
  59. package/data/harnesses/code/skills/design/data/color/clay-parchment.md +90 -0
  60. package/data/harnesses/code/skills/design/data/color/duolingo-feather.md +90 -0
  61. package/data/harnesses/code/skills/design/data/color/geist-mono-signal.md +90 -0
  62. package/data/harnesses/code/skills/design/data/color/linear-graphite-violet.md +90 -0
  63. package/data/harnesses/code/skills/design/data/color/mercury-onyx-cobalt.md +90 -0
  64. package/data/harnesses/code/skills/design/data/color/notion-paper-ink.md +90 -0
  65. package/data/harnesses/code/skills/design/data/color/radix-slate-iris.md +90 -0
  66. package/data/harnesses/code/skills/design/data/color/raycast-void-spectrum.md +90 -0
  67. package/data/harnesses/code/skills/design/data/color/spectrum-celery-fuchsia.md +90 -0
  68. package/data/harnesses/code/skills/design/data/color/stripe-blurple-ruby.md +90 -0
  69. package/data/harnesses/code/skills/design/data/color/superhuman-cod-picton.md +89 -0
  70. package/data/harnesses/code/skills/design/data/corpora/operations-review/companies.csv +15 -0
  71. package/data/harnesses/code/skills/design/data/corpora/operations-review/corpus.md +87 -0
  72. package/data/harnesses/code/skills/design/data/corpora/operations-review/people.csv +25 -0
  73. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/companies.csv +15 -0
  74. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/corpus.md +83 -0
  75. package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/people.csv +25 -0
  76. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/companies.csv +15 -0
  77. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/corpus.md +89 -0
  78. package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/people.csv +25 -0
  79. package/data/harnesses/code/skills/design/data/elevation/ant-design-directional-shadow.md +19 -0
  80. package/data/harnesses/code/skills/design/data/elevation/apple-liquid-glass-material.md +18 -0
  81. package/data/harnesses/code/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +20 -0
  82. package/data/harnesses/code/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +20 -0
  83. package/data/harnesses/code/skills/design/data/elevation/github-primer-sparing-shadow.md +19 -0
  84. package/data/harnesses/code/skills/design/data/elevation/ibm-carbon-layer-and-border.md +19 -0
  85. package/data/harnesses/code/skills/design/data/elevation/linear-dark-surface-ladder.md +17 -0
  86. package/data/harnesses/code/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +19 -0
  87. package/data/harnesses/code/skills/design/data/elevation/material-3-tonal-elevation.md +21 -0
  88. package/data/harnesses/code/skills/design/data/elevation/neobrutalist-hard-offset.md +19 -0
  89. package/data/harnesses/code/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +18 -0
  90. package/data/harnesses/code/skills/design/data/elevation/notion-warm-minimal-hairline.md +17 -0
  91. package/data/harnesses/code/skills/design/data/elevation/polaris-elevation-inset-bevel.md +21 -0
  92. package/data/harnesses/code/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +19 -0
  93. package/data/harnesses/code/skills/design/data/elevation/shadcn-neutral-derived-radius.md +19 -0
  94. package/data/harnesses/code/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +18 -0
  95. package/data/harnesses/code/skills/design/data/elevation/tailwind-default-utility-scale.md +19 -0
  96. package/data/harnesses/code/skills/design/data/elevation/vercel-geist-shadow-as-border.md +20 -0
  97. package/data/harnesses/code/skills/design/data/gradients/amber-attention-fill.md +16 -0
  98. package/data/harnesses/code/skills/design/data/gradients/cloud-shadow-wash.md +16 -0
  99. package/data/harnesses/code/skills/design/data/gradients/confident-lavender-wash.md +16 -0
  100. package/data/harnesses/code/skills/design/data/gradients/ember-ship-flare.md +18 -0
  101. package/data/harnesses/code/skills/design/data/gradients/full-metal-wash.md +16 -0
  102. package/data/harnesses/code/skills/design/data/gradients/indigo-ruby-cta-band.md +18 -0
  103. package/data/harnesses/code/skills/design/data/gradients/linear-graphite-wash.md +18 -0
  104. package/data/harnesses/code/skills/design/data/gradients/marble-quartz-wash.md +16 -0
  105. package/data/harnesses/code/skills/design/data/gradients/midnight-aurora-panel.md +16 -0
  106. package/data/harnesses/code/skills/design/data/gradients/neutral-volume-fill.md +16 -0
  107. package/data/harnesses/code/skills/design/data/gradients/revenue-uptrend-fill.md +16 -0
  108. package/data/harnesses/code/skills/design/data/gradients/risk-decline-fill.md +16 -0
  109. package/data/harnesses/code/skills/design/data/gradients/signal-blue-develop.md +18 -0
  110. package/data/harnesses/code/skills/design/data/gradients/slate-mist-wash.md +16 -0
  111. package/data/harnesses/code/skills/design/data/gradients/stripe-atmosphere-mesh.md +18 -0
  112. package/data/harnesses/code/skills/design/data/gradients/teal-signal-accent.md +16 -0
  113. package/data/harnesses/code/skills/design/data/gradients/violet-magenta-promo.md +16 -0
  114. package/data/harnesses/code/skills/design/data/iconography.md +199 -0
  115. package/data/harnesses/code/skills/design/data/motion/accordion-expand.md +18 -0
  116. package/data/harnesses/code/skills/design/data/motion/destructive-hold-confirm.md +18 -0
  117. package/data/harnesses/code/skills/design/data/motion/drag-dismiss-momentum.md +18 -0
  118. package/data/harnesses/code/skills/design/data/motion/empty-state-arrive.md +18 -0
  119. package/data/harnesses/code/skills/design/data/motion/hover-lift.md +18 -0
  120. package/data/harnesses/code/skills/design/data/motion/modal-center-arrive.md +18 -0
  121. package/data/harnesses/code/skills/design/data/motion/number-ticker-roll.md +18 -0
  122. package/data/harnesses/code/skills/design/data/motion/onboarding-bounce.md +18 -0
  123. package/data/harnesses/code/skills/design/data/motion/panel-slide.md +18 -0
  124. package/data/harnesses/code/skills/design/data/motion/press-feedback.md +18 -0
  125. package/data/harnesses/code/skills/design/data/motion/progress-linear-loop.md +18 -0
  126. package/data/harnesses/code/skills/design/data/motion/route-crossfade.md +18 -0
  127. package/data/harnesses/code/skills/design/data/motion/rubber-band-overscroll.md +18 -0
  128. package/data/harnesses/code/skills/design/data/motion/skeleton-shimmer.md +18 -0
  129. package/data/harnesses/code/skills/design/data/motion/stagger-arrive.md +18 -0
  130. package/data/harnesses/code/skills/design/data/motion/toast-enter-stack.md +18 -0
  131. package/data/harnesses/code/skills/design/data/motion/tooltip-chain-instant.md +18 -0
  132. package/data/harnesses/code/skills/design/data/motion/trigger-anchored-reveal.md +18 -0
  133. package/data/harnesses/code/skills/design/data/tells.csv +34 -0
  134. package/data/harnesses/code/skills/design/data/typography/anton-work-sans.md +17 -0
  135. package/data/harnesses/code/skills/design/data/typography/archivo-black-archivo.md +17 -0
  136. package/data/harnesses/code/skills/design/data/typography/bitter-karla.md +17 -0
  137. package/data/harnesses/code/skills/design/data/typography/dm-serif-dm-sans.md +17 -0
  138. package/data/harnesses/code/skills/design/data/typography/fraunces-inter.md +17 -0
  139. package/data/harnesses/code/skills/design/data/typography/geist-geist-mono.md +17 -0
  140. package/data/harnesses/code/skills/design/data/typography/hubot-sans-mona-sans.md +17 -0
  141. package/data/harnesses/code/skills/design/data/typography/ibm-plex-sans-operator-single-family.md +17 -0
  142. package/data/harnesses/code/skills/design/data/typography/instrument-serif-instrument-sans.md +17 -0
  143. package/data/harnesses/code/skills/design/data/typography/inter-operator-single-family.md +17 -0
  144. package/data/harnesses/code/skills/design/data/typography/libre-baskerville-libre-franklin.md +17 -0
  145. package/data/harnesses/code/skills/design/data/typography/newsreader-inter.md +17 -0
  146. package/data/harnesses/code/skills/design/data/typography/playfair-display-source-sans.md +17 -0
  147. package/data/harnesses/code/skills/design/data/typography/public-sans-operator-single-family.md +17 -0
  148. package/data/harnesses/code/skills/design/data/typography/sohne-inter-fintech.md +17 -0
  149. package/data/harnesses/code/skills/design/data/typography/source-serif-source-sans.md +17 -0
  150. package/data/harnesses/code/skills/design/data/typography/space-grotesk-work-sans-space-mono.md +17 -0
  151. package/data/harnesses/code/skills/design/data/typography/spectral-manrope.md +17 -0
  152. package/data/harnesses/code/skills/design/data/voices/bright-collaborator.md +29 -0
  153. package/data/harnesses/code/skills/design/data/voices/direct-operator.md +29 -0
  154. package/data/harnesses/code/skills/design/data/voices/earnest-helper.md +29 -0
  155. package/data/harnesses/code/skills/design/data/voices/editorial-composed.md +29 -0
  156. package/data/harnesses/code/skills/design/data/voices/quiet-concierge.md +29 -0
  157. package/data/harnesses/code/skills/design/data/voices/warm-guide.md +29 -0
  158. package/data/harnesses/code/skills/design/design-md/SCHEMA.md +179 -0
  159. package/data/harnesses/code/skills/design/design-md/index.csv +67 -0
  160. package/data/harnesses/code/skills/design/design-md/library/airbnb.md +207 -0
  161. package/data/harnesses/code/skills/design/design-md/library/airtable.md +205 -0
  162. package/data/harnesses/code/skills/design/design-md/library/anthropic-claude.md +203 -0
  163. package/data/harnesses/code/skills/design/design-md/library/apple.md +206 -0
  164. package/data/harnesses/code/skills/design/design-md/library/binance.md +210 -0
  165. package/data/harnesses/code/skills/design/design-md/library/bmw.md +210 -0
  166. package/data/harnesses/code/skills/design/design-md/library/cal.md +209 -0
  167. package/data/harnesses/code/skills/design/design-md/library/clay.md +204 -0
  168. package/data/harnesses/code/skills/design/design-md/library/clickhouse.md +205 -0
  169. package/data/harnesses/code/skills/design/design-md/library/cohere.md +205 -0
  170. package/data/harnesses/code/skills/design/design-md/library/coinbase.md +198 -0
  171. package/data/harnesses/code/skills/design/design-md/library/composio.md +198 -0
  172. package/data/harnesses/code/skills/design/design-md/library/cursor.md +197 -0
  173. package/data/harnesses/code/skills/design/design-md/library/elevenlabs.md +199 -0
  174. package/data/harnesses/code/skills/design/design-md/library/expo.md +200 -0
  175. package/data/harnesses/code/skills/design/design-md/library/figma.md +199 -0
  176. package/data/harnesses/code/skills/design/design-md/library/framer.md +202 -0
  177. package/data/harnesses/code/skills/design/design-md/library/hashicorp.md +205 -0
  178. package/data/harnesses/code/skills/design/design-md/library/ibm.md +205 -0
  179. package/data/harnesses/code/skills/design/design-md/library/intercom.md +204 -0
  180. package/data/harnesses/code/skills/design/design-md/library/kraken.md +200 -0
  181. package/data/harnesses/code/skills/design/design-md/library/linear-app.md +198 -0
  182. package/data/harnesses/code/skills/design/design-md/library/lovable.md +199 -0
  183. package/data/harnesses/code/skills/design/design-md/library/meta.md +204 -0
  184. package/data/harnesses/code/skills/design/design-md/library/minimax.md +204 -0
  185. package/data/harnesses/code/skills/design/design-md/library/mintlify.md +210 -0
  186. package/data/harnesses/code/skills/design/design-md/library/miro.md +209 -0
  187. package/data/harnesses/code/skills/design/design-md/library/mistral-ai.md +201 -0
  188. package/data/harnesses/code/skills/design/design-md/library/mongodb.md +201 -0
  189. package/data/harnesses/code/skills/design/design-md/library/notion.md +204 -0
  190. package/data/harnesses/code/skills/design/design-md/library/nvidia.md +215 -0
  191. package/data/harnesses/code/skills/design/design-md/library/ollama.md +213 -0
  192. package/data/harnesses/code/skills/design/design-md/library/opencode-ai.md +216 -0
  193. package/data/harnesses/code/skills/design/design-md/library/posthog.md +218 -0
  194. package/data/harnesses/code/skills/design/design-md/library/raycast.md +214 -0
  195. package/data/harnesses/code/skills/design/design-md/library/replicate.md +202 -0
  196. package/data/harnesses/code/skills/design/design-md/library/resend.md +203 -0
  197. package/data/harnesses/code/skills/design/design-md/library/revolut.md +203 -0
  198. package/data/harnesses/code/skills/design/design-md/library/runwayml.md +203 -0
  199. package/data/harnesses/code/skills/design/design-md/library/sanity.md +200 -0
  200. package/data/harnesses/code/skills/design/design-md/library/sentry.md +201 -0
  201. package/data/harnesses/code/skills/design/design-md/library/shopify.md +202 -0
  202. package/data/harnesses/code/skills/design/design-md/library/slack.md +202 -0
  203. package/data/harnesses/code/skills/design/design-md/library/spacex.md +206 -0
  204. package/data/harnesses/code/skills/design/design-md/library/spotify.md +206 -0
  205. package/data/harnesses/code/skills/design/design-md/library/stripe.md +196 -0
  206. package/data/harnesses/code/skills/design/design-md/library/supabase.md +196 -0
  207. package/data/harnesses/code/skills/design/design-md/library/superhuman.md +194 -0
  208. package/data/harnesses/code/skills/design/design-md/library/tesla.md +198 -0
  209. package/data/harnesses/code/skills/design/design-md/library/theverge.md +199 -0
  210. package/data/harnesses/code/skills/design/design-md/library/together-ai.md +193 -0
  211. package/data/harnesses/code/skills/design/design-md/library/uber.md +195 -0
  212. package/data/harnesses/code/skills/design/design-md/library/vercel.md +195 -0
  213. package/data/harnesses/code/skills/design/design-md/library/voltagent.md +197 -0
  214. package/data/harnesses/code/skills/design/design-md/library/warp.md +196 -0
  215. package/data/harnesses/code/skills/design/design-md/library/webflow.md +197 -0
  216. package/data/harnesses/code/skills/design/design-md/library/wired.md +197 -0
  217. package/data/harnesses/code/skills/design/design-md/library/wise.md +198 -0
  218. package/data/harnesses/code/skills/design/design-md/library/x-ai.md +199 -0
  219. package/data/harnesses/code/skills/design/design-md/library/zapier.md +198 -0
  220. package/data/harnesses/code/skills/design/design-md/nurix.md +214 -0
  221. package/data/harnesses/code/skills/design/design-md/variants/editorial-luxury.md +184 -0
  222. package/data/harnesses/code/skills/design/design-md/variants/expressive-saas.md +216 -0
  223. package/data/harnesses/code/skills/design/design-md/variants/quiet-premium.md +211 -0
  224. package/data/harnesses/code/skills/design/design-md/variants/terse-dense-operator.md +212 -0
  225. package/data/harnesses/code/skills/design/design-md/variants/warm-approachable.md +216 -0
  226. package/data/harnesses/code/skills/design/references/compose.md +23 -0
  227. package/data/harnesses/code/skills/design/references/content.md +28 -0
  228. package/data/harnesses/code/skills/design/references/emission.md +21 -0
  229. package/data/harnesses/code/skills/design/references/existing-apps.md +27 -0
  230. package/data/harnesses/code/skills/design/references/intake.md +36 -0
  231. package/data/harnesses/code/skills/design/references/object-pass.md +24 -0
  232. package/data/harnesses/code/skills/design/references/ship-gate.md +29 -0
  233. package/data/harnesses/code/skills/design/references/ux-patterns/Readme.md +195 -0
  234. package/data/harnesses/code/skills/design/references/ux-patterns/ai-layer.md +49 -0
  235. package/data/harnesses/code/skills/design/references/ux-patterns/attributes-lifecycle.md +43 -0
  236. package/data/harnesses/code/skills/design/references/ux-patterns/container-choice.md +44 -0
  237. package/data/harnesses/code/skills/design/references/ux-patterns/creation-paths.md +46 -0
  238. package/data/harnesses/code/skills/design/references/ux-patterns/dashboards.md +42 -0
  239. package/data/harnesses/code/skills/design/references/ux-patterns/modes.md +42 -0
  240. package/data/harnesses/code/skills/design/references/ux-patterns/object-model.md +46 -0
  241. package/data/harnesses/code/skills/design/references/ux-patterns/page-archetype.md +48 -0
  242. package/data/harnesses/code/skills/design/references/ux-patterns/promotion-demotion.md +37 -0
  243. package/data/harnesses/code/skills/design/references/ux-patterns/settings.md +42 -0
  244. package/data/harnesses/code/skills/design/references/ux-patterns/standard-pages.md +46 -0
  245. package/data/harnesses/code/skills/design/references/ux-patterns/state-matrix.md +41 -0
  246. package/data/harnesses/code/skills/design/references/ux-patterns/table-mechanics.md +48 -0
  247. package/data/harnesses/code/skills/design/references/ux-patterns/verbs.md +45 -0
  248. package/data/harnesses/code/skills/design/references/ux-patterns/viewer-context.md +40 -0
  249. package/data/harnesses/code/skills/design/scripts/README-validator.md +27 -0
  250. package/data/harnesses/code/skills/design/scripts/select-chart.mjs +596 -0
  251. package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +1198 -0
  252. package/package.json +1 -1
  253. package/data/harnesses/code/skills/design/data/catalog/rules.csv +0 -137
  254. package/data/harnesses/code/skills/design/data/combinations/Readme.md +0 -33
  255. package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +0 -113
  256. package/data/harnesses/code/skills/design/data/combinations/charts.csv +0 -33
  257. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +0 -448
  258. package/data/harnesses/code/skills/design/data/combinations/icons.csv +0 -157
  259. package/data/harnesses/code/skills/design/data/combinations/landing.csv +0 -38
  260. package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +0 -2001
  261. package/data/harnesses/code/skills/design/data/combinations/manifest.json +0 -330
  262. package/data/harnesses/code/skills/design/data/combinations/motion.csv +0 -10
  263. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +0 -92
  264. package/data/harnesses/code/skills/design/data/combinations/product-router.csv +0 -80
  265. package/data/harnesses/code/skills/design/data/combinations/styles.csv +0 -67
  266. package/data/harnesses/code/skills/design/data/combinations/typography.csv +0 -64
  267. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +0 -106
  268. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +0 -154
  269. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +0 -151
  270. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +0 -139
  271. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +0 -139
  272. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +0 -140
  273. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +0 -140
  274. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +0 -140
  275. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +0 -139
  276. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +0 -139
  277. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +0 -147
  278. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +0 -156
  279. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +0 -138
  280. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +0 -138
  281. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +0 -137
  282. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -148
  283. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -141
  284. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +0 -156
  285. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +0 -140
  286. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +0 -153
  287. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +0 -158
  288. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +0 -156
  289. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -122
  290. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +0 -140
  291. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +0 -139
  292. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +0 -139
  293. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +0 -139
  294. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +0 -153
  295. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +0 -140
  296. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +0 -155
  297. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +0 -140
  298. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +0 -141
  299. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +0 -145
  300. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +0 -142
  301. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +0 -152
  302. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +0 -137
  303. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +0 -140
  304. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +0 -170
  305. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +0 -145
  306. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -137
  307. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +0 -168
  308. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +0 -163
  309. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +0 -147
  310. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +0 -158
  311. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +0 -171
  312. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +0 -166
  313. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +0 -160
  314. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +0 -173
  315. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +0 -145
  316. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +0 -153
  317. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +0 -167
  318. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +0 -140
  319. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +0 -140
  320. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +0 -140
  321. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +0 -130
  322. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +0 -140
  323. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +0 -140
  324. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +0 -139
  325. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +0 -137
  326. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +0 -138
  327. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +0 -158
  328. package/data/harnesses/code/skills/design/data/design-md/nurix.md +0 -138
  329. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +0 -695
  330. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +0 -20
  331. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +0 -1272
  332. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +0 -20
  333. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +0 -1268
  334. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +0 -20
  335. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +0 -1273
  336. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +0 -20
  337. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +0 -1262
  338. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +0 -20
  339. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +0 -1257
  340. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +0 -20
  341. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +0 -1270
  342. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +0 -20
  343. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +0 -1270
  344. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +0 -20
  345. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +0 -1261
  346. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +0 -20
  347. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +0 -1268
  348. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +0 -20
  349. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +0 -1281
  350. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +0 -20
  351. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +0 -1262
  352. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +0 -20
  353. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +0 -1271
  354. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +0 -20
  355. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +0 -1276
  356. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +0 -20
  357. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +0 -1293
  358. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +0 -20
  359. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +0 -1270
  360. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +0 -20
  361. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +0 -1276
  362. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +0 -20
  363. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +0 -1267
  364. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +0 -251
  365. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +0 -20
  366. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +0 -1273
  367. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +0 -20
  368. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +0 -1286
  369. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +0 -20
  370. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +0 -1290
  371. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +0 -20
  372. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +0 -1270
  373. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +0 -23
  374. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +0 -599
  375. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +0 -1272
  376. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +0 -20
  377. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +0 -1267
  378. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +0 -20
  379. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +0 -1289
  380. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +0 -20
  381. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +0 -1258
  382. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +0 -20
  383. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +0 -1284
  384. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +0 -20
  385. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +0 -1291
  386. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +0 -20
  387. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +0 -1263
  388. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +0 -20
  389. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +0 -1270
  390. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +0 -20
  391. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +0 -1261
  392. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +0 -20
  393. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +0 -1209
  394. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +0 -20
  395. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +0 -1265
  396. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +0 -20
  397. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +0 -1253
  398. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +0 -20
  399. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +0 -1268
  400. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +0 -20
  401. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +0 -1261
  402. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +0 -20
  403. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +0 -1263
  404. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +0 -20
  405. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +0 -1266
  406. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +0 -23
  407. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +0 -623
  408. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +0 -1276
  409. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +0 -20
  410. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +0 -1264
  411. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +0 -20
  412. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +0 -1268
  413. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +0 -20
  414. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +0 -1270
  415. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +0 -20
  416. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +0 -1288
  417. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +0 -20
  418. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +0 -1272
  419. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +0 -20
  420. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +0 -1265
  421. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +0 -20
  422. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +0 -1287
  423. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +0 -20
  424. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +0 -1286
  425. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +0 -20
  426. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +0 -1285
  427. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +0 -20
  428. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +0 -1287
  429. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +0 -20
  430. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +0 -1278
  431. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +0 -20
  432. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +0 -1269
  433. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +0 -20
  434. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +0 -1285
  435. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +0 -20
  436. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +0 -1303
  437. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +0 -20
  438. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +0 -1274
  439. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +0 -20
  440. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +0 -1296
  441. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +0 -23
  442. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +0 -612
  443. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +0 -1282
  444. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +0 -20
  445. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +0 -1287
  446. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +0 -20
  447. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +0 -1268
  448. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +0 -20
  449. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +0 -1276
  450. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +0 -20
  451. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +0 -1273
  452. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +0 -20
  453. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +0 -1296
  454. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +0 -20
  455. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +0 -1286
  456. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +0 -249
  457. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +0 -249
  458. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +0 -225
  459. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +0 -249
  460. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +0 -224
  461. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +0 -249
  462. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +0 -88
  463. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +0 -132
  464. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +0 -76
  465. package/data/harnesses/code/skills/design/references/create.md +0 -173
  466. package/data/harnesses/code/skills/design/references/diagnosis.md +0 -108
  467. package/data/harnesses/code/skills/design/references/hard-rules.md +0 -177
  468. package/data/harnesses/code/skills/design/references/improve.md +0 -116
  469. package/data/harnesses/code/skills/design/references/interaction.md +0 -77
  470. package/data/harnesses/code/skills/design/references/introspect.md +0 -100
  471. package/data/harnesses/code/skills/design/references/lifecycle.md +0 -73
  472. package/data/harnesses/code/skills/design/references/personality.md +0 -575
  473. package/data/harnesses/code/skills/design/references/structure.md +0 -159
  474. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +0 -1312
  475. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +0 -1498
  476. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +0 -551
  477. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +0 -1092
  478. package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +0 -26
  479. package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +0 -65
  480. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +0 -538
  481. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +0 -312
  482. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +0 -890
  483. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +0 -769
  484. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +0 -675
  485. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +0 -164
  486. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +0 -439
@@ -1,1092 +0,0 @@
1
- // skills/design/scripts/lib/checks.mjs — the machine-check engine (beam 2, D19): every detector=source catalog rule as pure regex/token analysis over .tsx/.ts/.css/.html text, plus the lever-manifest drift check (cod-02, D10) whose vocabulary is read from the project's design-tokens artifact (design.tokens.json). Pure library — no argv, no CLI banner, no process.exit; fired at structural boundaries by phase-gate.mjs, preship-sweep.mjs, and self-integrity.mjs. Findings are [{rule, file, line, excerpt, fix}].
2
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { findBudgetStamps, MOTION_STAGGER_CLASS, STAGGER_CHILD_CAP } from './vocabulary.mjs';
5
-
6
- // ---------------------------------------------------------------------------
7
- // File-type routing
8
- // ---------------------------------------------------------------------------
9
-
10
- export const CHECKABLE_EXTS = ['.tsx', '.ts', '.css', '.html'];
11
-
12
- // Detect the file's extension bucket, or null when it is not a checkable type.
13
- export function extOf(path) {
14
- const m = path.toLowerCase().match(/\.(tsx|ts|css|html)$/);
15
- return m ? m[1] : null;
16
- }
17
-
18
- // Recursively collect checkable UI files under a dir, skipping node_modules and dotfiles.
19
- export function walkUiFiles(dir, acc = []) {
20
- if (!existsSync(dir)) return acc;
21
- let entries;
22
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
23
- for (const ent of entries) {
24
- if (ent.name === 'node_modules' || ent.name.startsWith('.')) continue;
25
- const full = join(dir, ent.name);
26
- if (ent.isDirectory()) walkUiFiles(full, acc);
27
- else if (CHECKABLE_EXTS.some((e) => ent.name.endsWith(e))) acc.push(full);
28
- }
29
- return acc;
30
- }
31
-
32
- // Read the passed files into check-ready docs, skipping unreadable and uncheckable ones.
33
- export function loadDocs(files) {
34
- const docs = [];
35
- for (const file of files) {
36
- const ext = extOf(file);
37
- if (!ext) continue;
38
- let text;
39
- try { text = readFileSync(file, 'utf8'); } catch { continue; }
40
- docs.push({ file, ext, text, lines: text.split('\n') });
41
- }
42
- return docs;
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Color parsing (zero-dep): hex / rgb / hsl / oklch -> saturation signal
47
- // ---------------------------------------------------------------------------
48
-
49
- // Expand a 3- or 4-nibble hex to its 6/8-nibble form; return the lowercased hex digits.
50
- function expandHex(hex) {
51
- let h = hex.replace('#', '').toLowerCase();
52
- if (h.length === 3 || h.length === 4) h = h.split('').map((c) => c + c).join('');
53
- return h;
54
- }
55
-
56
- // Convert an r/g/b triple (0-255) to HSL saturation and lightness (both 0-1).
57
- function rgbToSl(r, g, b) {
58
- const rr = r / 255, gg = g / 255, bb = b / 255;
59
- const max = Math.max(rr, gg, bb), min = Math.min(rr, gg, bb);
60
- const l = (max + min) / 2;
61
- const d = max - min;
62
- const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
63
- return { s, l };
64
- }
65
-
66
- // Parse one color literal into { sat, light } (HSL, 0-1) or { oklchChroma } or null.
67
- function parseColor(lit) {
68
- const s = lit.trim().toLowerCase();
69
- if (s.startsWith('#')) {
70
- const h = expandHex(s.match(/#[0-9a-f]{3,8}/i)?.[0] || s);
71
- if (h.length < 6) return null;
72
- const r = parseInt(h.slice(0, 2), 16);
73
- const g = parseInt(h.slice(2, 4), 16);
74
- const b = parseInt(h.slice(4, 6), 16);
75
- if ([r, g, b].some(Number.isNaN)) return null;
76
- const { s: sat, l } = rgbToSl(r, g, b);
77
- return { sat, light: l };
78
- }
79
- const rgb = s.match(/rgba?\(([^)]*)\)/);
80
- if (rgb) {
81
- const parts = rgb[1].split(/[\s,/]+/).filter(Boolean).slice(0, 3).map(Number);
82
- if (parts.length < 3 || parts.some(Number.isNaN)) return null;
83
- const { s: sat, l } = rgbToSl(parts[0], parts[1], parts[2]);
84
- return { sat, light: l };
85
- }
86
- const hsl = s.match(/hsla?\(([^)]*)\)/);
87
- if (hsl) {
88
- const parts = hsl[1].split(/[\s,/]+/).filter(Boolean);
89
- if (parts.length < 3) return null;
90
- const sat = parseFloat(parts[1]) / 100;
91
- const light = parseFloat(parts[2]) / 100;
92
- if (Number.isNaN(sat) || Number.isNaN(light)) return null;
93
- return { sat, light };
94
- }
95
- const okl = s.match(/oklch\(([^)]*)\)/);
96
- if (okl) {
97
- const parts = okl[1].split(/[\s,/]+/).filter(Boolean);
98
- if (parts.length < 2) return null;
99
- const chroma = parseFloat(parts[1]);
100
- if (Number.isNaN(chroma)) return null;
101
- return { oklchChroma: chroma };
102
- }
103
- return null;
104
- }
105
-
106
- // Decide whether a parsed color exceeds the accent saturation ceiling (col-02, <80%).
107
- function overAccentSaturation(parsed) {
108
- if (!parsed) return false;
109
- if (typeof parsed.oklchChroma === 'number') return parsed.oklchChroma > 0.37 * 0.8; // 0.296 heuristic
110
- if (parsed.light == null || parsed.light < 0.12 || parsed.light > 0.92) return false; // sat is unstable near black/white
111
- return parsed.sat >= 0.8;
112
- }
113
-
114
- // Normalize a color literal for set membership (drift compare): strip inner spaces, lowercase, expand short hex.
115
- function normColor(lit) {
116
- let s = lit.trim().toLowerCase().replace(/\s+/g, '');
117
- const hx = s.match(/^#[0-9a-f]{3,8}$/);
118
- if (hx) return '#' + expandHex(s);
119
- return s;
120
- }
121
-
122
- // ---------------------------------------------------------------------------
123
- // Value regexes shared by several rules
124
- // ---------------------------------------------------------------------------
125
-
126
- const RE_COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/g;
127
-
128
- // CSS generic + system family keywords — never counted as declared app families (typ-06);
129
- // narrower than SYSTEM_FONT_KEYWORDS, whose web-safe named fonts (Roboto, Arial…) DO count.
130
- const GENERIC_FAMILY_KEYWORDS = new Set([
131
- 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy', 'math', 'emoji', 'fangsong',
132
- 'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
133
- '-apple-system', 'blinkmacsystemfont', 'sfmono-regular',
134
- ]);
135
-
136
- // Base signature of a color literal — the first three channels with the alpha slot stripped,
137
- // hex unified onto rgb — so an emitted color whose alpha rides var()/calc() (the intensity-
138
- // multiplied shadow layers, which RE_COLOR_LITERAL truncates mid-calc) can still be matched
139
- // against the manifest vocabulary.
140
- function colorBaseSig(lit) {
141
- const s = lit.trim().toLowerCase();
142
- if (/^#[0-9a-f]{3,8}$/.test(s)) {
143
- const h = expandHex(s);
144
- return `rgb:${parseInt(h.slice(0, 2), 16)},${parseInt(h.slice(2, 4), 16)},${parseInt(h.slice(4, 6), 16)}`;
145
- }
146
- const fn = s.match(/^(rgba?|hsla?|oklch)\(([^/)]*)/);
147
- if (!fn) return null;
148
- const nums = fn[2].match(/-?\d*\.?\d+%?/g);
149
- if (!nums || nums.length < 3) return null;
150
- const kind = fn[1].startsWith('rgb') ? 'rgb' : fn[1].startsWith('hsl') ? 'hsl' : 'oklch';
151
- return `${kind}:${nums.slice(0, 3).join(',')}`;
152
- }
153
- const TW_UTILITY = '(?:bg|text|border|ring|ring-offset|fill|stroke|from|via|to|outline|divide|decoration|caret|accent|placeholder|shadow)';
154
- const PALETTE_HUES = 'red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|grey|zinc|neutral|stone';
155
- const RE_PALETTE_CLASS = new RegExp(`\\b${TW_UTILITY}-(${PALETTE_HUES})-(\\d{2,3})\\b`, 'g');
156
- const STATUS_HUES = new Set(['red', 'rose', 'orange', 'amber', 'yellow', 'green', 'emerald', 'lime']);
157
-
158
- // A spacing value in px is on the 4/8 scale (or the dense 2px step) when it is 0, 2, or a multiple of 4.
159
- function spacingOnScale(px) { return px === 0 || px === 2 || px % 4 === 0; }
160
- const RADIUS_SCALE = new Set([0, 2, 4, 6, 8, 12, 16, 24, 9999]);
161
- const ZINDEX_SCALE = new Set([0, 10, 20, 40, 100, 1000]);
162
-
163
- // Convert a rem or px length token to px (16px root); return null when it is neither.
164
- function toPx(numStr, unit) {
165
- const n = parseFloat(numStr);
166
- if (Number.isNaN(n)) return null;
167
- if (unit === 'rem') return n * 16;
168
- if (unit === 'px' || unit === '') return n;
169
- return null;
170
- }
171
-
172
- // ---------------------------------------------------------------------------
173
- // Findings sink
174
- // ---------------------------------------------------------------------------
175
-
176
- // Build a findings collector that de-dupes on (rule, file, line, excerpt).
177
- export function makeSink() {
178
- const seen = new Set();
179
- const list = [];
180
- return {
181
- add(rule, file, line, excerpt, fix) {
182
- const ex = (excerpt || '').trim().slice(0, 200);
183
- const key = `${rule}|${file}|${line}|${ex}`;
184
- if (seen.has(key)) return;
185
- seen.add(key);
186
- list.push({ rule, file, line, excerpt: ex, fix });
187
- },
188
- list,
189
- };
190
- }
191
-
192
- // ---------------------------------------------------------------------------
193
- // Per-line checks (run once per source line)
194
- // ---------------------------------------------------------------------------
195
-
196
- // Run every single-line source check against one line and record any violations.
197
- export function checkLine(add, file, ext, lineNo, line, fileText) {
198
- const isJsx = ext === 'tsx' || ext === 'ts' || ext === 'html';
199
- const isCss = ext === 'css';
200
-
201
- // --- Color ---
202
- if (isJsx) {
203
- // col-05: literal palette classes, hex/rgb/oklch literals, and dark: prefixes in JSX.
204
- for (const m of line.matchAll(RE_PALETTE_CLASS)) {
205
- add('col-05', file, lineNo, m[0], 'Replace the literal palette class with a semantic token class (bg-primary, text-muted-foreground, border-border).');
206
- if (STATUS_HUES.has(m[1])) add('col-04', file, lineNo, m[0], 'Route status color through a semantic token: --success, --warning, or --destructive (e.g. text-success), never a raw palette hue.');
207
- }
208
- if (/\bdark:/.test(line)) add('col-05', file, lineNo, (line.match(/\bdark:[\w[\]/.-]+/) || ['dark:'])[0], 'Drop the dark: prefix; light is the default theme and dark is opted in at the token layer, not per class.');
209
- for (const m of line.matchAll(RE_COLOR_LITERAL)) {
210
- add('col-05', file, lineNo, m[0], 'Move the color literal into a semantic token; components consume tokens (bg-card, text-foreground), never raw hex/rgb/oklch.');
211
- }
212
- }
213
- // col-02: accent saturation ceiling. JSX: any literal; CSS: only accent-role token definitions.
214
- if (isJsx) {
215
- for (const m of line.matchAll(RE_COLOR_LITERAL)) {
216
- if (overAccentSaturation(parseColor(m[0]))) add('col-02', file, lineNo, m[0], 'Desaturate the accent below the 80% saturation ceiling (or oklch chroma <= 0.30); reserve one restrained accent.');
217
- }
218
- } else if (isCss && /--(?:primary|accent|ring)\b/.test(line)) {
219
- for (const m of line.matchAll(RE_COLOR_LITERAL)) {
220
- if (overAccentSaturation(parseColor(m[0]))) add('col-02', file, lineNo, m[0], 'The accent/primary/ring token exceeds the 80% saturation ceiling (oklch chroma <= 0.30); pull it down a step.');
221
- }
222
- }
223
- // col-06: pure black / pure white literals.
224
- {
225
- const pm = line.match(/#000000\b|#000\b|#ffffff\b|#fff\b|rgb\(\s*0\s*,\s*0\s*,\s*0\s*\)|rgb\(\s*255\s*,\s*255\s*,\s*255\s*\)/i);
226
- if (pm) add('col-06', file, lineNo, pm[0], 'Never pure #000000/#ffffff; start near #111827/#1F2937 for ink and an off-white surface, declared as a token.');
227
- }
228
- // col-07: opacity- or rgba-fabricated shades (rgba/hsla with alpha, 8-digit hex).
229
- {
230
- const fm = line.match(/#[0-9a-fA-F]{8}\b|rgba\([^)]*\)|hsla\([^)]*\)/);
231
- if (fm && !/\b(?:overlay|scrim|backdrop|modal)\b/i.test(line)) add('col-07', file, lineNo, fm[0], 'Do not fabricate a shade via alpha; declare the shade as an explicit hex/oklch token instead.');
232
- }
233
- // col-09: disabled-state opacity band (0.38-0.5).
234
- for (const m of line.matchAll(/(?:disabled:opacity-|opacity-)(\d{1,3})\b/g)) {
235
- if (!/disabled/.test(line)) continue;
236
- const o = parseInt(m[1], 10) / 100;
237
- if (o < 0.38 || o > 0.5) add('col-09', file, lineNo, m[0], 'Set the disabled opacity into the 0.38-0.5 muted band (Tailwind opacity-40/45).');
238
- }
239
- // col-10: modal scrim opacity band (40-60% black).
240
- if (/\b(?:overlay|scrim|backdrop)\b/i.test(line)) {
241
- for (const m of line.matchAll(/bg-black\/(\d{1,3})\b/g)) {
242
- const p = parseInt(m[1], 10);
243
- if (p < 40 || p > 60) add('col-10', file, lineNo, m[0], 'Put the modal scrim in the 40-60% black band (bg-black/50).');
244
- }
245
- }
246
- // col-13: semantic status color used as a solid fill (never a fill).
247
- {
248
- const cm = line.match(/\bbg-(?:success|warning)\b|\bbg-(?:green|emerald|amber|yellow|orange|rose)-\d{2,3}\b/);
249
- if (cm && !/\/\d/.test(cm[0])) add('col-13', file, lineNo, cm[0], 'Status/price color is text-only, never a solid fill; use the token as text/border (text-success) or a low-opacity tint on a badge.');
250
- }
251
-
252
- // --- Typography ---
253
- // typ-02: any explicit font-size below 11px.
254
- for (const m of line.matchAll(/(?:font-size\s*:\s*|text-\[)(\d*\.?\d+)(px|rem)?/g)) {
255
- const px = toPx(m[1], m[2] || 'px');
256
- if (px != null && px < 11) add('typ-02', file, lineNo, m[0], 'Never render below 11px; body sits at 14px+ on operator surfaces.');
257
- }
258
- // typ-03: input font-size below 16px, only on a touch/mobile surface (scope-guarded).
259
- if (/<(?:input|textarea)\b/i.test(line) && /(?:touch|mobile|viewport-fit|maximum-scale)/i.test(fileText)) {
260
- for (const m of line.matchAll(/text-\[(\d*\.?\d+)(px|rem)?\]/g)) {
261
- const px = toPx(m[1], m[2] || 'px');
262
- if (px != null && px < 16) add('typ-03', file, lineNo, m[0], 'Form inputs on touch/mobile surfaces are >= 16px to defeat iOS zoom.');
263
- }
264
- }
265
- // typ-05: prose measure declared in ch, outside 45-75.
266
- for (const m of line.matchAll(/(?:max-width\s*:\s*|max-w-\[)(\d+)ch/g)) {
267
- const ch = parseInt(m[1], 10);
268
- if (ch < 45 || ch > 75) add('typ-05', file, lineNo, m[0], 'Keep prose measure in the 45-75ch readable band (data tables are exempt).');
269
- }
270
- // typ-08: uppercase micro-label tracking and the negative tracking floor.
271
- {
272
- const neg = line.match(/(?:letter-spacing\s*:\s*|tracking-\[)(-?\d*\.?\d+)em/);
273
- if (neg && parseFloat(neg[1]) < -0.04) add('typ-08', file, lineNo, neg[0], 'Letter-spacing never below the -0.04em floor.');
274
- // A `--tracking-tighter: var(…)` @theme line REMAPS the utility onto an authored token —
275
- // that definition is the fix for this rule, not a violation; literal values still fall
276
- // to the numeric floor check above.
277
- if (/\btracking-tighter\b/.test(line) && !/--tracking-tighter\s*:\s*var\(/.test(line)) add('typ-08', file, lineNo, 'tracking-tighter', 'tracking-tighter (-0.05em) is below the -0.04em floor; use tracking-tight at most.');
278
- if (/\buppercase\b/.test(line) && !/tracking-(?:wide|wider|widest|\[)/.test(line)) add('typ-08', file, lineNo, 'uppercase', 'All-caps micro-labels carry positive tracking (>= 0.02em, caps band 0.05-0.12em); add tracking-wide.');
279
- }
280
- // typ-09: viewport-fluid font sizing.
281
- {
282
- const vm = line.match(/(?:font-size\s*:[^;]*|text-\[[^\]]*)(?:\d*\.?\d+(?:vw|vh)|clamp\()/);
283
- if (vm) add('typ-09', file, lineNo, vm[0], 'Font sizes come from a fixed rem scale; ban vw/vh/clamp on font-size.');
284
- }
285
- // typ-11: UI font weight below 400.
286
- {
287
- const wm = line.match(/(?:font-weight\s*:\s*|font-\[)(\d{3})\]?/);
288
- if (wm && parseInt(wm[1], 10) < 400) add('typ-11', file, lineNo, wm[0], 'No UI font weight below 400.');
289
- if (/\bfont-(?:thin|extralight|light)\b/.test(line)) add('typ-11', file, lineNo, (line.match(/font-(?:thin|extralight|light)/) || [''])[0], 'No UI font weight below 400 (font-thin/extralight/light are 100-300).');
290
- }
291
- // typ-12: type sized in em.
292
- {
293
- const em = line.match(/(?:font-size\s*:\s*|text-\[)(\d*\.?\d+)em/);
294
- if (em) add('typ-12', file, lineNo, em[0], 'Never size type in em (it compounds); use px or rem.');
295
- }
296
- // typ-16: serif font family (not sans-serif).
297
- {
298
- const sf = line.match(/font-?family\s*:\s*[^;}]*\bserif\b|font-serif\b/i);
299
- if (sf && !/sans-serif/i.test(sf[0])) add('typ-16', file, lineNo, sf[0].trim(), 'Serif faces are banned in dashboards/software UIs; use the sans family.');
300
- }
301
- // typ-19: mono/code type sized below 16px.
302
- if (/font-mono\b|font-family\s*:[^;]*monospace|<(?:code|pre)\b/i.test(line)) {
303
- for (const m of line.matchAll(/text-\[(\d*\.?\d+)(px|rem)?\]|text-(xs|sm)\b/g)) {
304
- if (m[3]) { add('typ-19', file, lineNo, m[0], 'Code/mono type is fixed at 16px; long lines scroll horizontally, never shrink.'); continue; }
305
- const px = toPx(m[1], m[2] || 'px');
306
- if (px != null && px < 16) add('typ-19', file, lineNo, m[0], 'Code/mono type is fixed at 16px; long lines scroll horizontally, never shrink.');
307
- }
308
- }
309
-
310
- // --- Spatial ---
311
- // spc-01: explicit (arbitrary/CSS) spacing value off the 4/8 scale.
312
- for (const m of line.matchAll(/(?:[pm][trblxy]?|gap(?:-[xy])?|space-[xy]|inset)-\[(\d*\.?\d+)(px|rem)?\]/g)) {
313
- const px = toPx(m[1], m[2] || 'px');
314
- if (px != null && !spacingOnScale(px)) add('spc-01', file, lineNo, m[0], `Land the spacing on the 4/8 scale (nearest step to ${px}px); ${px}px is off-scale.`);
315
- }
316
- if (isCss) {
317
- for (const m of line.matchAll(/(?:padding|margin|gap|top|left|right|bottom)\s*:\s*([^;]+);/g)) {
318
- for (const v of m[1].matchAll(/(\d*\.?\d+)(px|rem)/g)) {
319
- const px = toPx(v[1], v[2]);
320
- if (px != null && px !== 1 && !spacingOnScale(px)) add('spc-01', file, lineNo, v[0], `Land the spacing on the 4/8 scale (nearest step to ${px}px); ${px}px is off-scale.`);
321
- }
322
- }
323
- }
324
- // spc-04: center-aligned data column.
325
- if (/<t[dh]\b/i.test(line) && /\btext-center\b/.test(line)) add('spc-04', file, lineNo, 'text-center', 'Numeric columns right-align, text left-aligns; never center-align a data column.');
326
- // spc-06: corner radius off the fixed scale.
327
- for (const m of line.matchAll(/(?:border-radius\s*:\s*|rounded-\[)(\d*\.?\d+)(px|rem)?/g)) {
328
- const px = toPx(m[1], m[2] || 'px');
329
- if (px != null && !RADIUS_SCALE.has(px)) add('spc-06', file, lineNo, m[0], `Corners land on the fixed radius scale (0/2/4/6/8/12/16/24/full); ${px}px is off-scale.`);
330
- }
331
- // spc-07: a button taller than the 28px dense-row cap sitting inside a table row.
332
- if (/<t[dh]\b/i.test(line) && /<[Bb]utton\b/.test(line)) {
333
- const bh = line.match(/\bh-(?:8|9|10|11|12|14|16|20)\b|\bh-\[(\d+)px\]|\bmin-h-\[(\d+)px\]/);
334
- if (bh) {
335
- const px = (bh[1] || bh[2]) ? parseInt(bh[1] || bh[2], 10) : 99;
336
- if (px > 28) add('spc-07', file, lineNo, bh[0], 'Dense table rows compact their buttons to 28px (h-7); trim the control height and keep mono numerals on figures.');
337
- }
338
- }
339
- // spc-08: full-viewport height via h-screen / 100vh instead of dvh.
340
- {
341
- const hm = line.match(/\b(?:min-)?h-screen\b|100vh|h-\[100vh\]/);
342
- if (hm) add('spc-08', file, lineNo, hm[0], 'Full-viewport height uses min-h-[100dvh]; never h-screen or 100vh.');
343
- }
344
- // spc-09: calc-percentage layout arithmetic.
345
- {
346
- const cm = line.match(/calc\([^)]*%[^)]*\)/);
347
- if (cm) add('spc-09', file, lineNo, cm[0], 'Ban calc-percentage layout arithmetic; use CSS Grid instead.');
348
- }
349
-
350
- // --- Depth ---
351
- // dep-01: z-index off the declared scale, or the banned 999/9999.
352
- for (const m of line.matchAll(/(?:z-index\s*:\s*|z-\[)(\d{1,4})/g)) {
353
- const z = parseInt(m[1], 10);
354
- if (z === 999 || z === 9999 || !ZINDEX_SCALE.has(z)) add('dep-01', file, lineNo, m[0], 'z-index only from the declared scale (0/10/20/40/100/1000); ban 999 and 9999.');
355
- }
356
- // dep-04: a border and a heavy elevation shadow doing the same separation job.
357
- if (/\bborder(?:-[24]|-solid|-\w+-\d)?\b/.test(line) && /\bshadow-(?:lg|xl|2xl)\b/.test(line)) {
358
- add('dep-04', file, lineNo, (line.match(/shadow-(?:lg|xl|2xl)/) || [''])[0], 'Border and shadow are doing the same separation job; keep one (a hairline border or a shadow), not both heavy.');
359
- }
360
-
361
- // --- Interaction ---
362
- // int-12: a non-button clickable element without cursor:pointer.
363
- if (/<(?:div|span|li)\b[^>]*\bonClick=/.test(line) && !/cursor-pointer/.test(line)) {
364
- add('int-12', file, lineNo, (line.match(/<(?:div|span|li)\b[^>]*onClick=[^ >]*/) || ['onClick'])[0], 'cursor:pointer on every clickable element; add cursor-pointer (or use a real <button>).');
365
- }
366
- // int-03: a hover-revealed control with no keyboard/focus path.
367
- if (/(?:group-hover|hover):(?:opacity-100|block|flex|visible)\b/.test(line) && !/(?:group-focus-within|focus-within|focus-visible|group-focus):/.test(fileText)) {
368
- add('int-03', file, lineNo, (line.match(/(?:group-hover|hover):(?:opacity-100|block|flex|visible)/) || [''])[0], 'No pointer-only affordances; give the hover-revealed control a focus/keyboard path (group-focus-within:).');
369
- }
370
- // int-04: a modal built on a div role="dialog" instead of native <dialog>.
371
- if (/<div\b[^>]*role=["']dialog["']/.test(line)) add('int-04', file, lineNo, 'role="dialog"', 'Modals use native <dialog> with inert focus containment; prefer a side panel before a modal.');
372
- // int-08: placeholder-as-label on a non-search input.
373
- if (/<input\b/i.test(line) && /placeholder=/.test(line) && !/aria-label|aria-labelledby|type=["']search["']/.test(line) && !/placeholder=["'][^"']*[Ss]earch/.test(line) && !/<label/i.test(fileText)) {
374
- add('int-08', file, lineNo, (line.match(/placeholder=["'][^"']*["']/) || ['placeholder'])[0], 'Labels stay visible above the input; ban placeholder-as-label (errors render inline below).');
375
- }
376
- // int-09: a sortable header with no aria-sort.
377
- if (/<th\b/i.test(line) && /(?:onClick=|cursor-pointer|sortable)/.test(line) && !/aria-sort/.test(line)) {
378
- add('int-09', file, lineNo, '<th> sortable', 'Sortable headers carry aria-sort; give the sortable <th> an aria-sort state.');
379
- }
380
- // int-11: a button group with a sub-8px gap between targets.
381
- {
382
- const gm = line.match(/\b(?:gap|space-x)-\[(\d*\.?\d+)px\]|\b(?:gap|space-x)-0(?:\.5)?\b|gap-px\b/);
383
- if (gm && /<button|role=["']button["']|<a\b/.test(line)) {
384
- const n = gm[1] != null ? parseFloat(gm[1]) : 0;
385
- if (n < 8) add('int-11', file, lineNo, gm[0], 'Target-to-target spacing clears the 8px minimum gap.');
386
- }
387
- }
388
- // int-02: outline removed with no replacement focus style anywhere in the file.
389
- if (/\boutline-none\b|outline\s*:\s*none/.test(line) && !/(?:focus-visible:|focus:ring|:focus-visible|ring-[12]|focus:outline-\[)/.test(fileText)) {
390
- add('int-02', file, lineNo, (line.match(/outline-none|outline\s*:\s*none/) || [''])[0], 'Ban outline:none without a replacement; add a :focus-visible ring (focus-visible:ring-2).');
391
- }
392
- // int-14: a raw button styling hover but neither focus nor disabled states.
393
- if (/<button\b/i.test(line) && /hover:/.test(line) && !/(?:focus-visible:|focus:)/.test(line) && !/disabled:/.test(line)) {
394
- add('int-14', file, lineNo, '<button>', 'All five button states are required: default, hover, active, focus, disabled; this button is missing focus/disabled.');
395
- }
396
-
397
- // --- Motion ---
398
- // mot-01: an animation/transition duration over the 500ms ceiling.
399
- for (const m of line.matchAll(/duration-\[(\d+)ms\]|duration-(700|1000)\b|(?:transition|animation)(?:-duration)?\s*:[^;]*?(\d*\.?\d+)(ms|s)/g)) {
400
- let ms = null;
401
- if (m[1]) ms = parseInt(m[1], 10);
402
- else if (m[2]) ms = parseInt(m[2], 10);
403
- else if (m[3]) ms = m[4] === 's' ? parseFloat(m[3]) * 1000 : parseFloat(m[3]);
404
- if (ms != null && ms > 500) add('mot-01', file, lineNo, m[0], `UI transitions hold under the 500ms ceiling; ${ms}ms is too long.`);
405
- }
406
- // mot-04: banned easing (spring/bounce on operator surfaces, linear everywhere).
407
- {
408
- const lm = line.match(/\bease-linear\b|transition-timing-function\s*:\s*linear|animation-timing-function\s*:\s*linear/);
409
- if (lm) add('mot-04', file, lineNo, lm[0], 'Ban linear easing; use the token ease-out (cubic-bezier(0,0,0.2,1)).');
410
- if (/cubic-bezier\(\s*-?\d/.test(line)) {
411
- const cb = line.match(/cubic-bezier\([^)]*\)/)[0];
412
- const nums = cb.match(/-?\d*\.?\d+/g).map(Number);
413
- if (nums.some((n) => n < 0 || n > 1)) add('mot-04', file, lineNo, cb, 'Ban spring/bounce easing (control points outside 0-1) on operator surfaces; use the token ease-out.');
414
- }
415
- if (/\b(?:ease-spring|ease-bounce|spring\()/.test(line)) add('mot-04', file, lineNo, (line.match(/ease-spring|ease-bounce|spring\(/) || [''])[0], 'Ban spring/bounce easing on operator surfaces; use the token ease-out.');
416
- }
417
- // mot-05: animating a layout property instead of transform/opacity.
418
- {
419
- const am = line.match(/transition-(?:all|left|top|width|height|margin)\b|transition\s*:\s*[^;]*\b(?:left|top|width|height|margin)\b|animate-\[[^\]]*\b(?:left|top|width|height|margin)\b/);
420
- if (am) add('mot-05', file, lineNo, am[0], 'Animate only transform and opacity, never top/left/width/height/margin (transition-all included).');
421
- }
422
- // mot-06: will-change on a layout property or left standing without an animation.
423
- {
424
- const wm = line.match(/will-change\s*:\s*([^;]+)|will-change-\[?([\w,-]+)/);
425
- if (wm) {
426
- const val = (wm[1] || wm[2] || '').toLowerCase();
427
- if (/left|top|width|height|margin|auto|all|scroll/.test(val) || (!/transform|opacity/.test(val))) {
428
- add('mot-06', file, lineNo, wm[0], 'will-change only rides an in-progress transform/opacity animation; never blanket, never on a layout property, never left dangling.');
429
- }
430
- }
431
- }
432
- // mot-07: a loading indicator delay outside the 150-300ms band.
433
- if (/skeleton|spinner|loading/i.test(line)) {
434
- for (const m of line.matchAll(/(?:animation-delay|transition-delay|delay-\[)\s*:?\s*(\d+)ms/g)) {
435
- const d = parseInt(m[1], 10);
436
- if (d > 0 && (d < 150 || d > 300)) add('mot-07', file, lineNo, m[0], 'Loading-indicator delay sits in the 150-300ms band (nothing appears under 500ms).');
437
- }
438
- }
439
- // mot-08 (numeric half): the per-item stagger unit holds the 0-60ms band, a declared cascade never
440
- // reaches past the child cap, and no stagger delay lands at or over the 1s sequence ceiling. Child
441
- // count and the actual total are confirmed on the rendered DOM at the render rung.
442
- {
443
- const unit = line.match(/--stagger-unit\s*:\s*(-?\d*\.?\d+)ms/);
444
- if (unit && (parseFloat(unit[1]) < 0 || parseFloat(unit[1]) > 60)) {
445
- add('mot-08', file, lineNo, unit[0], 'The stagger-unit token holds 0-60ms per item; pull the unit back into the band.');
446
- }
447
- const idxDelay = line.match(/(?:animationDelay|animation-delay|transitionDelay|transition-delay)[^;,}]*?(?:\b(?:i|idx|index)\s*\*\s*(\d*\.?\d+)|(\d*\.?\d+)\s*\*\s*(?:i|idx|index)\b)/);
448
- if (idxDelay && parseFloat(idxDelay[1] ?? idxDelay[2]) > 60) {
449
- add('mot-08', file, lineNo, idxDelay[0].slice(0, 60), 'Per-item stagger delay exceeds the 0-60ms unit band; derive it from the stagger-unit token (0-60ms).');
450
- }
451
- const calcUnit = line.match(/(?:animation|transition)-delay\s*:\s*calc\(\s*(\d*\.?\d+)ms\s*\*/);
452
- if (calcUnit && parseFloat(calcUnit[1]) > 60) {
453
- add('mot-08', file, lineNo, calcUnit[0], 'Per-item stagger delay exceeds the 0-60ms unit band; ride the stagger-unit token instead of a raw multiplier.');
454
- }
455
- if (/(?:animation|transition)-delay/.test(line)) {
456
- const nth = line.match(/nth-child\((\d+)\)/);
457
- if (nth && parseInt(nth[1], 10) > STAGGER_CHILD_CAP) {
458
- add('mot-08', file, lineNo, nth[0], `Stagger cascades cap at ${STAGGER_CHILD_CAP} children; children past the cap arrive together.`);
459
- }
460
- if (/stagger|nth-child/i.test(line)) {
461
- for (const m of line.matchAll(/(?:animation|transition)-delay\s*:\s*(\d*\.?\d+)(ms|s)\b/g)) {
462
- const ms = m[2] === 's' ? parseFloat(m[1]) * 1000 : parseFloat(m[1]);
463
- if (ms >= 1000) add('mot-08', file, lineNo, m[0], 'A stagger sequence totals under 1s; this per-item delay alone reaches the ceiling.');
464
- }
465
- }
466
- }
467
- }
468
- // mot-09: scroll-event listener driving animation.
469
- if (/addEventListener\(\s*["']scroll["']/.test(line)) add('mot-09', file, lineNo, 'addEventListener("scroll")', 'Ban scroll-event listeners for animation; use IntersectionObserver or scroll-linked scrubbing.');
470
- // mot-10: useState fed by a continuous scroll/pointer value.
471
- if (/set\w+\(/.test(line) && /on(?:Scroll|MouseMove|PointerMove)=/.test(line)) {
472
- add('mot-10', file, lineNo, (line.match(/on(?:Scroll|MouseMove|PointerMove)=[^ >]*/) || [''])[0], 'Ban useState for continuous scroll/pointer values; drive them with motion values instead.');
473
- }
474
-
475
- // --- States ---
476
- // sta-05: a toast duration outside the 3-5s band.
477
- for (const m of line.matchAll(/duration\s*[:=]\s*\{?\s*(\d{3,5})/g)) {
478
- if (!/toast|Toaster|sonner/i.test(line)) continue;
479
- const d = parseInt(m[1], 10);
480
- if (d < 3000 || d > 5000) add('sta-05', file, lineNo, m[0], 'Toasts auto-dismiss in the 3-5s band, announce politely (aria-live="polite"), and never steal focus.');
481
- }
482
-
483
- // --- Code & tokens ---
484
- // cod-03: a raw color/spacing/radius/duration literal inside an inline style object.
485
- if (isJsx && /style=\{\{/.test(line)) {
486
- const sm = line.match(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|oklch\([^)]*\)|:\s*['"]?\d+px|:\s*['"]?\d+ms/);
487
- if (sm) add('cod-03', file, lineNo, sm[0], 'Every color/spacing/radius/duration resolves to an emitted token; do not inline a raw literal in a style prop.');
488
- }
489
- // cod-04: elision comments and brevity markers.
490
- {
491
- const cm = line.match(/\/\/\s*\.\.\.|\/\/\s*(?:rest of|remaining|existing)\b|\{\s*\/\*\s*\.\.\.|\/\*\s*\.\.\.\s*\*\/|\bfor brevity\b|\/\/\s*TODO\b/i);
492
- if (cm) add('cod-04', file, lineNo, cm[0], 'No elision/brevity/TODO comments; emit the whole file, compiling.');
493
- }
494
- // cod-05: fabricated names, placeholder companies, and fake round metrics.
495
- {
496
- const fm = line.match(/\bJohn Doe\b|\bJane Doe\b|\bAcme\b|\bLorem ipsum\b|\bBY THE NUMBERS\b|\b99\.9[89]%|\b100% (?:uptime|guaranteed)\b/i);
497
- if (fm) add('cod-05', file, lineNo, fm[0], 'Anti-fabrication: no invented metrics or placeholder names; use real data, a [bracket] placeholder, or mark mock numbers.');
498
- }
499
- // cod-07: a Google Fonts <link>/@import in production.
500
- if (/fonts\.googleapis\.com|fonts\.gstatic\.com/.test(line)) add('cod-07', file, lineNo, (line.match(/https?:\/\/fonts\.[^"'\s)]+/) || ['fonts.googleapis.com'])[0], 'Never a Google Fonts link in prod; self-host or use next/font with font-display: swap.');
501
- // cod-08: an <img> hotlinked or without declared dimensions / lazy-load.
502
- if (/<img\b/i.test(line)) {
503
- if (!/\bwidth=|\bheight=/.test(line)) add('cod-08', file, lineNo, '<img>', 'Images declare width/height (avoid CLS); below-the-fold images add loading="lazy".');
504
- const src = line.match(/src=["'](https?:\/\/[^"']+)["']/);
505
- if (src && !/picsum\.photos\/seed\//.test(src[1])) add('cod-08', file, lineNo, src[1], 'Image placeholders are deterministic seeds (picsum.photos/seed/{seed}/{w}/{h}), never a live hotlink.');
506
- }
507
- // cod-06: emoji character in source.
508
- if (RE_EMOJI.test(line)) add('cod-06', file, lineNo, (line.match(RE_EMOJI) || [''])[0], 'No emoji anywhere and no emoji-as-icons; use inline SVG from one icon family.');
509
- RE_EMOJI.lastIndex = 0;
510
- }
511
-
512
- const RE_EMOJI = /[\u{1F300}-\u{1FAFF}\u{1F1E6}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE0F}]/u;
513
-
514
- // Blank out // line comments and /* */ (incl. JSX {/* */}) block comments, preserving line count, so copy checks skip commentary.
515
- export function cleanComments(lines) {
516
- const out = [];
517
- let inBlock = false;
518
- for (let raw of lines) {
519
- let s = raw;
520
- if (inBlock) {
521
- const end = s.indexOf('*/');
522
- if (end < 0) { out.push(''); continue; }
523
- s = ' '.repeat(end + 2) + s.slice(end + 2);
524
- inBlock = false;
525
- }
526
- // strip inline /* ... */ occurrences
527
- s = s.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length));
528
- const openBlock = s.indexOf('/*');
529
- if (openBlock >= 0 && s.indexOf('*/', openBlock) < 0) { s = s.slice(0, openBlock); inBlock = true; }
530
- // strip // line comment when not part of :// (a URL scheme)
531
- const lc = s.match(/(^|[^:])\/\/.*$/);
532
- if (lc) s = s.slice(0, lc.index + lc[1].length);
533
- out.push(s);
534
- }
535
- return out;
536
- }
537
-
538
- // Scan comment-stripped copy for em-dashes (typ-15) and three-period ellipses (typ-14).
539
- export function checkCopy(add, file, cleaned) {
540
- cleaned.forEach((line, i) => {
541
- if (line.includes('—')) add('typ-15', file, i + 1, line.trim(), 'Zero em-dashes in UI copy; recast the sentence or use a colon/en-dash.');
542
- const el = line.match(/\.\.\.(?![\w$.([{])/);
543
- if (el) add('typ-14', file, i + 1, '...', 'Use the ellipsis glyph (or an explicit loading label), not three periods.');
544
- });
545
- }
546
-
547
- // ---------------------------------------------------------------------------
548
- // Whole-file checks (need more than one line of context)
549
- // ---------------------------------------------------------------------------
550
-
551
- // Run the file-scoped source checks over one file's full text.
552
- export function checkFile(add, file, ext, text, lines) {
553
- const isJsx = ext === 'tsx' || ext === 'ts';
554
-
555
- // dep-02: more than one <h1> in a view.
556
- const h1s = [...text.matchAll(/<h1\b/gi)];
557
- if (h1s.length > 1) {
558
- const idx = text.slice(0, h1s[1].index).split('\n').length;
559
- add('dep-02', file, idx, `${h1s.length} <h1> elements`, 'One h1 per view; keep a single <h1> and never skip heading levels.');
560
- }
561
-
562
- // mot-03: an animation present with no prefers-reduced-motion guard.
563
- const hasAnim = /@keyframes\b|\banimate-(?!none\b)[\w[\]-]+|\.animate\(|animation\s*:/i.test(text);
564
- const hasGuard = /prefers-reduced-motion|motion-reduce:|useReducedMotion/i.test(text);
565
- if (hasAnim && !hasGuard) {
566
- const idx = (text.match(/@keyframes|animate-|animation\s*:/i) ? text.slice(0, text.search(/@keyframes|animate-|animation\s*:/i)).split('\n').length : 1);
567
- add('mot-03', file, idx, 'animation without reduced-motion guard', 'Every animation is gated on prefers-reduced-motion (0.01ms reset / motion-reduce:).');
568
- }
569
-
570
- // typ-07: a numeric table context with no tabular figures anywhere in the file.
571
- if (isJsx && /<td\b/i.test(text)) {
572
- const numeric = /toLocaleString\(|toFixed\(|Intl\.NumberFormat|\$\{?[\w.]+\}?\s*(?:USD|amount|price|total)|\bformatCurrency\b/i.test(text);
573
- const tabular = /tabular-nums|font-variant-numeric|lining-nums|slashed-zero|tnum/i.test(text);
574
- if (numeric && !tabular) {
575
- const idx = text.slice(0, text.search(/<td\b/i)).split('\n').length;
576
- add('typ-07', file, idx, 'numeric table cells without tabular-nums', 'Tabular lining numerals (tabular-nums) on every numeric column, KPI, timer, and ID.');
577
- }
578
- }
579
-
580
- // sta-01: a collection render with none of loading/empty/error handling.
581
- // A module-local literal array (e.g. a static nav constant) is not a data
582
- // surface — only collections that arrive as data (imports, props, hooks,
583
- // fetches) demand lifecycle states.
584
- if (isJsx && /\.map\(/.test(text)) {
585
- const hasState = /length\s*===?\s*0|isEmpty|Empty|isLoading|\bloading\b|Skeleton|Spinner|isError|\berror\b|catch\b/i.test(text);
586
- if (!hasState) {
587
- const mapRe = /(\w+)(?:\.\w+)*\.map\(/g;
588
- let m;
589
- let firstDataMapIdx = -1;
590
- while ((m = mapRe.exec(text)) !== null) {
591
- const localLiteral = new RegExp(`const\\s+${m[1]}(?:\\s*:\\s*[^=]+)?=\\s*\\[`).test(text);
592
- if (!localLiteral) { firstDataMapIdx = m.index; break; }
593
- }
594
- if (firstDataMapIdx !== -1) {
595
- const idx = text.slice(0, firstDataMapIdx).split('\n').length;
596
- add('sta-01', file, idx, 'collection render with no loading/empty/error state', 'Every data surface ships loading, empty, error, and partial/degraded states; none are present here.');
597
- }
598
- }
599
- }
600
- // sta-02: an empty state that dead-ends (literal no-data message) or carries >1 action.
601
- if (isJsx && /(?:isEmpty|length\s*===?\s*0|Empty)/i.test(text)) {
602
- const deadEnd = text.match(/>\s*(?:No data(?:\s+available)?|Nothing (?:here|to show)|No results found\.?)\s*</i);
603
- if (deadEnd) add('sta-02', file, text.slice(0, deadEnd.index).split('\n').length, deadEnd[0].trim(), 'Empty states are text-first with a next step, never a literal no-data dead-end; 1-2 sentences and at most one action.');
604
- }
605
- // sta-03: an error-state render that dead-ends — no recovery affordance in the rendered error block.
606
- // The three-part error copy (what / why / how) stays judged; this shell flags only the mechanical dead
607
- // end that an error state offers the operator no way forward. Matches the canonical error-render forms
608
- // (an early return, an inline &&/? render, or a catch that returns JSX) and looks for any recovery
609
- // affordance (retry / reload / link / action / handler) in the block that follows.
610
- if (isJsx) {
611
- const errorRender = /if\s*\(\s*(?:isError|hasError|loadError)\b[^)]*\)\s*(?:\{[^}]*)?return|(?:isError|hasError|loadError)\s*(?:&&|\?)\s*\(?\s*<|if\s*\(\s*status\s*===?\s*['"](?:error|failed)['"][^)]*\)\s*(?:\{[^}]*)?return|\}\s*catch\s*(?:\([^)]*\))?\s*\{[^}]*return\s*[<(]/g;
612
- const RECOVERY = /onClick|onRetry|onReload|onRefresh|href=|<a\b|<Button\b|<button\b|\bretry\b|\breload\b|\brefresh\b|\brefetch\b|try again|go back/i;
613
- let em;
614
- while ((em = errorRender.exec(text)) !== null) {
615
- const windowText = text.slice(em.index, em.index + 600);
616
- if (!RECOVERY.test(windowText)) {
617
- add('sta-03', file, text.slice(0, em.index).split('\n').length, 'error state with no recovery affordance', 'Error states never dead-end: state what happened, why, and how to recover, and give a way forward (retry, reload, or a link out) — not a bare failure message.');
618
- break;
619
- }
620
- }
621
- }
622
-
623
- // typ-20 / col-16 / mot-08 (orchestration half): budget-gated vocabulary against the surface's
624
- // budget stamp (lib/vocabulary.mjs). The display ramp step, wash consumption, and arrival cascades
625
- // are legal only on a file stamped brand-forward; a baseline-stamped or unstamped file consuming
626
- // them is the deterministic violation. Fired at the surfaces gate and the preship sweep through
627
- // this shared engine.
628
- if (ext === 'tsx' || ext === 'html') {
629
- const brandForward = findBudgetStamps(text).includes('brand-forward');
630
- const lineOf = (idx) => text.slice(0, idx).split('\n').length;
631
- const displayUses = [...text.matchAll(/\btext-display\b|var\(--text-display\b/g)];
632
- if (displayUses.length && !brandForward) {
633
- add('typ-20', file, lineOf(displayUses[0].index), displayUses[0][0], 'The display ramp step appears only on surfaces stamped brand-forward; re-classify the surface with a recorded reason, or step this element down the ramp (text-h1).');
634
- } else if (displayUses.length > 1) {
635
- add('typ-20', file, lineOf(displayUses[1].index), `${displayUses.length} display-step elements`, 'At most one display-step element per surface; keep the focal one and step the rest down the ramp.');
636
- }
637
- const washUse = text.match(/var\(--wash-[a-z-]+\)/);
638
- if (washUse && !brandForward) {
639
- add('col-16', file, lineOf(washUse.index), washUse[0], 'A wash token is consumed only on brand-forward-stamped surfaces; re-classify the surface with a recorded reason, or remove the wash.');
640
- }
641
- const staggerUse = text.match(new RegExp(`\\b${MOTION_STAGGER_CLASS}\\b`));
642
- if (staggerUse && !brandForward) {
643
- add('mot-08', file, lineOf(staggerUse.index), staggerUse[0], 'Baseline-stamped surfaces skip arrival cascades; remove motion-stagger, or re-classify the surface with a recorded reason.');
644
- }
645
- }
646
-
647
- // int-01: a :focus / :focus-visible CSS rule that removes the outline with no visible ring replacement.
648
- if (ext === 'css') {
649
- for (const m of text.matchAll(/:focus(?:-visible)?\s*\{([^}]*)\}/g)) {
650
- const body = m[1];
651
- const removesOutline = /outline\s*:\s*(?:none|0)\b/.test(body);
652
- const hasRing = /box-shadow\s*:\s*[^;]*\d/.test(body) || /outline-width\s*:/.test(body) || /outline-offset\s*:/.test(body) || /outline\s*:\s*[^;]*(?:px|rem|em|solid|auto|\d)/i.test(body) || /border(?:-\w+)?\s*:\s*[^;]*(?:px|solid|\d)/i.test(body) || /\bring-/.test(body);
653
- if (removesOutline && !hasRing) add('int-01', file, text.slice(0, m.index).split('\n').length, m[0].replace(/\s+/g, ' ').slice(0, 60), 'Every interactive element ships a visible :focus-visible ring (>= 2px with offset); add a ring, do not merely remove the outline.');
654
- }
655
- }
656
-
657
- // int-10: live-updating status (polling) with no aria-live region present.
658
- if (isJsx && /setInterval\(/.test(text) && !/aria-live|role=["'](?:status|alert)["']/.test(text)) {
659
- add('int-10', file, text.slice(0, text.search(/setInterval\(/)).split('\n').length, 'polling status update without aria-live', 'An aria-live region is present wherever async status updates render; wrap the live status in role="status" / aria-live="polite".');
660
- }
661
-
662
- // cod-01: a token-defining CSS file that omits some of the 36 consumed tokens. The trigger
663
- // requires DEFINITIONS at property position — a consumer file's `--color-background: var(--background)`
664
- // @theme mapping must not read as a token-defining file.
665
- if (ext === 'css' && /^\s*--background\s*:/m.test(text) && /^\s*--foreground\s*:/m.test(text)) {
666
- const missing = REQUIRED_TOKENS.filter((t) => !new RegExp(`--${t}\\s*:`).test(text));
667
- if (missing.length) add('cod-01', file, 1, `missing tokens: ${missing.slice(0, 6).join(', ')}${missing.length > 6 ? '…' : ''}`, 'Emitted CSS defines every token the substrate consumes; add the missing --token definitions.');
668
- }
669
-
670
- // cod-09: a component file well over the line ceiling.
671
- if (isJsx && lines.length > 250) add('cod-09', file, lines.length, `${lines.length} lines`, 'Component line ceilings hold (< 120 ln table, < 80 ln section); split this file.');
672
-
673
- // cod-11: an exported component that takes props with no typed Props.
674
- if (ext === 'tsx') {
675
- const untyped = text.match(/export\s+(?:default\s+)?function\s+[A-Z]\w*\(\s*\{[^}]*\}\s*\)(?!\s*:)/) ||
676
- text.match(/export\s+(?:default\s+)?function\s+[A-Z]\w*\(\s*props\s*\)/);
677
- const hasTypedProps = /interface\s+\w*Props\b|type\s+\w*Props\b|:\s*\w*Props\b|React\.FC</.test(text);
678
- if (untyped && !hasTypedProps) {
679
- add('cod-11', file, text.slice(0, untyped.index).split('\n').length, untyped[0].slice(0, 60), 'Every emitted component exports a typed Props interface; annotate the props.');
680
- }
681
- }
682
- }
683
-
684
- const REQUIRED_TOKENS = [
685
- 'background', 'foreground', 'card', 'card-foreground', 'muted', 'muted-foreground', 'border', 'input', 'ring',
686
- 'primary', 'primary-foreground', 'accent', 'accent-foreground', 'destructive', 'destructive-foreground', 'success', 'warning',
687
- 'surface-raised', 'surface-overlay',
688
- 'popover', 'popover-foreground', 'secondary', 'secondary-foreground',
689
- 'chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5',
690
- 'sidebar', 'sidebar-foreground', 'sidebar-primary', 'sidebar-primary-foreground',
691
- 'sidebar-accent', 'sidebar-accent-foreground', 'sidebar-border', 'sidebar-ring',
692
- ];
693
-
694
- // ---------------------------------------------------------------------------
695
- // Cross-file checks (aggregate over all files passed this invocation)
696
- // ---------------------------------------------------------------------------
697
-
698
- // Run the checks that need the whole passed-in file set (token-set counts, distinct sizes).
699
- export function checkCrossFile(add, docs) {
700
- // typ-06: more than three font families declared across the passed files (fourth banned), or three
701
- // families none of which carries the numeric-mono job.
702
- const families = new Set();
703
- const monoFamilies = new Set();
704
- const iconLibs = new Set();
705
- const explicitSizes = new Map(); // px -> {file,line,text}
706
- const hexByHue = new Map(); // hue bucket -> Set(normHex)
707
- let sectionCount = 0;
708
- let capsTrackCount = 0;
709
- let capsTrackAnchor = null;
710
-
711
- for (const d of docs) {
712
- d.lines.forEach((line, i) => {
713
- // font families from declarations / --font-* family slots / font-[Family].
714
- // --font-weight-*/--font-size-* carry numbers, not families; a stack whose first entry
715
- // is a CSS generic (the emitted baseline's generics-only stacks) declares no family.
716
- for (const m of line.matchAll(/font-family\s*:\s*([^;{}]+)[;}]?|--font-(?!weight-|size-)[\w-]+\s*:\s*([^;]+);|font-\[["']?([A-Za-z][\w\s-]+)["']?\]/g)) {
717
- const raw = (m[1] || m[2] || m[3] || '').trim();
718
- const first = raw.split(',')[0].replace(/["']/g, '').trim().toLowerCase();
719
- if (first && !/^var\(/.test(first) && !GENERIC_FAMILY_KEYWORDS.has(first)) {
720
- families.add(first);
721
- // The numeric-mono job: declared as the mono slot or stacked onto a monospace generic.
722
- if (/^--font-mono/.test(m[0]) || /\b(?:ui-)?monospace\b/i.test(raw)) monoFamilies.add(first);
723
- }
724
- }
725
- // icon libraries from imports
726
- const imp = line.match(/from\s+["'](lucide-react|@heroicons\/\w+|react-icons(?:\/\w+)?|@phosphor-icons\/\w+|phosphor-react)["']/);
727
- if (imp) iconLibs.add(imp[1].split('/')[0]);
728
- // explicit numeric font sizes (arbitrary tailwind or CSS), for typ-10
729
- for (const m of line.matchAll(/text-\[(\d*\.?\d+)(px|rem)\]|font-size\s*:\s*(\d*\.?\d+)(px|rem)/g)) {
730
- const px = m[1] ? toPx(m[1], m[2]) : toPx(m[3], m[4]);
731
- if (px != null && !explicitSizes.has(px)) explicitSizes.set(px, { file: d.file, line: i + 1, text: (m[0]) });
732
- }
733
- // hex color families for col-08 over-count
734
- for (const m of line.matchAll(/#[0-9a-fA-F]{6}\b/g)) {
735
- const hue = hexHueBucket(m[0]);
736
- if (!hexByHue.has(hue)) hexByHue.set(hue, new Set());
737
- hexByHue.get(hue).add(normColor(m[0]));
738
- }
739
- // typ-18 section + caps-tracking tally
740
- if (/<section\b/i.test(line)) sectionCount += (line.match(/<section\b/gi) || []).length;
741
- if (/\buppercase\b/.test(line) && /tracking-(?:wide|wider|widest)/.test(line)) {
742
- capsTrackCount++;
743
- if (!capsTrackAnchor) capsTrackAnchor = { file: d.file, line: i + 1 };
744
- }
745
- });
746
- }
747
-
748
- if (families.size > 3) {
749
- const d = docs[0];
750
- add('typ-06', d.file, 1, `${families.size} font families: ${[...families].join(', ')}`, 'At most three font families per app, each with a distinct job (display / body / numeric-mono); a fourth family is banned — drop the extra.');
751
- } else if (families.size === 3 && monoFamilies.size === 0) {
752
- const d = docs[0];
753
- add('typ-06', d.file, 1, `3 font families, none mono: ${[...families].join(', ')}`, 'Three families need three distinct jobs (display / body / numeric-mono); none of these is a declared mono — give the third family a real job or drop it.');
754
- }
755
-
756
- // typ-06 (scale half): distinct explicit sizes beyond the declared ramp's step count.
757
- if (explicitSizes.size > 7) {
758
- const anchorPx = [...explicitSizes.keys()].sort((a, b) => a - b)[7];
759
- const anc = explicitSizes.get(anchorPx);
760
- add('typ-06', anc.file, anc.line, `${explicitSizes.size} distinct explicit sizes`, 'The type scale holds the declared ramp\'s steps (<= 7); collapse the extras onto the ramp utilities (text-h1 … text-display).');
761
- }
762
-
763
- // typ-10: adjacent explicitly-declared type-scale steps closer than 25%.
764
- const sizes = [...explicitSizes.keys()].sort((a, b) => a - b);
765
- if (sizes.length >= 3) {
766
- for (let i = 1; i < sizes.length; i++) {
767
- if (sizes[i] / sizes[i - 1] < 1.25) {
768
- const anc = explicitSizes.get(sizes[i]);
769
- add('typ-10', anc.file, anc.line, `${sizes[i - 1]}px and ${sizes[i]}px`, 'Adjacent type-scale steps stay >= 25% apart; merge these near-duplicate steps.');
770
- }
771
- }
772
- }
773
- // spc-02 is checked with the same near-duplicate logic over explicit spacing (checkSpacingScale).
774
-
775
- // col-08: a color family exceeding the 10-shade ceiling.
776
- for (const [hue, set] of hexByHue) {
777
- if (set.size > 10) {
778
- const d = docs[0];
779
- add('col-08', d.file, 1, `${set.size} ${hue} shades`, 'Shade counts stay bounded (greys 8-10, each accent family 5-10); this family exceeds 10.');
780
- }
781
- }
782
-
783
- // typ-18: uppercase-tracking instances beyond ceil(sectionCount/3).
784
- if (sectionCount > 0 && capsTrackCount > Math.ceil(sectionCount / 3) && capsTrackAnchor) {
785
- add('typ-18', capsTrackAnchor.file, capsTrackAnchor.line, `${capsTrackCount} caps-tracked labels vs ${sectionCount} sections`, 'Ration eyebrow/micro-label uppercase-tracking against section count (<= ceil(sectionCount/3)).');
786
- }
787
-
788
- if (iconLibs.size > 1) {
789
- const d = docs[0];
790
- add('cod-06', d.file, 1, `icon families: ${[...iconLibs].join(', ')}`, 'One icon family per app; consolidate onto a single family (lucide).');
791
- }
792
- }
793
-
794
- // Run spc-02 over explicit spacing values collected across files.
795
- export function checkSpacingScale(add, docs) {
796
- const spacings = new Map();
797
- for (const d of docs) {
798
- d.lines.forEach((line, i) => {
799
- for (const m of line.matchAll(/(?:[pm][trblxy]?|gap(?:-[xy])?|space-[xy]|inset)-\[(\d*\.?\d+)(px|rem)\]/g)) {
800
- const px = toPx(m[1], m[2]);
801
- if (px != null && !spacings.has(px)) spacings.set(px, { file: d.file, line: i + 1 });
802
- }
803
- });
804
- }
805
- const vals = [...spacings.keys()].sort((a, b) => a - b);
806
- if (vals.length >= 3) {
807
- for (let i = 1; i < vals.length; i++) {
808
- if (vals[i] / vals[i - 1] < 1.25) {
809
- const anc = spacings.get(vals[i]);
810
- add('spc-02', anc.file, anc.line, `${vals[i - 1]}px and ${vals[i]}px`, 'Adjacent spacing values stay >= 25% apart; these declared steps are too close.');
811
- }
812
- }
813
- }
814
- }
815
-
816
- // CSS generic families, the ui-* keywords, and the conventional OS fallback faces that need no
817
- // package to render — the only stack entries cod-16 accepts without a registering import/@font-face.
818
- // Exported so the fail-closed cod-16 gate at derive-personality --emit (stack vs package.json
819
- // binding) reads the same vocabulary as this source shell.
820
- export const SYSTEM_FONT_KEYWORDS = new Set([
821
- 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy', 'math', 'emoji', 'fangsong',
822
- 'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
823
- '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'roboto', 'helvetica', 'helvetica neue', 'arial',
824
- 'sfmono-regular', 'sf mono', 'menlo', 'consolas', 'courier new', 'liberation mono', 'monaco',
825
- 'georgia', 'times new roman', 'inherit', 'initial', 'unset',
826
- ]);
827
-
828
- // Slugify a family name the way a fontsource package specifier spells it ("Inter Variable" -> inter).
829
- export function familySlug(name) {
830
- return name.toLowerCase().replace(/\s+variable$/, '').trim().replace(/[^a-z0-9]+/g, '-');
831
- }
832
-
833
- // cod-16 (source shell): every named family in an emitted font stack binds to a registered family or
834
- // a system generic. Registration is an @font-face declaring the family or an @import whose package
835
- // specifier carries the family's slug (@fontsource/<slug>). The check arms only when the passed file
836
- // set carries at least one registration — the no-fonts-css-at-all case belongs to the fail-closed
837
- // package.json binding at derive-personality --emit, which owns this rule's gate.
838
- export function checkFontBinding(add, docs) {
839
- const cssDocs = docs.filter((d) => d.ext === 'css');
840
- const importSpecs = [];
841
- const registered = new Set();
842
- for (const d of cssDocs) {
843
- for (const m of d.text.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)) importSpecs.push(m[1].toLowerCase());
844
- for (const m of d.text.matchAll(/@font-face\s*\{[^}]*font-family\s*:\s*["']?([^;"'}]+)/g)) registered.add(m[1].trim().toLowerCase());
845
- }
846
- if (!importSpecs.length && !registered.size) return;
847
- const isBound = (name) => {
848
- const n = name.toLowerCase();
849
- if (SYSTEM_FONT_KEYWORDS.has(n) || registered.has(n)) return true;
850
- const slug = familySlug(name);
851
- return slug !== '' && importSpecs.some((spec) => spec.includes(`/${slug}`) || spec === slug);
852
- };
853
- for (const d of cssDocs) {
854
- d.lines.forEach((line, i) => {
855
- for (const m of line.matchAll(/(?:--font-[\w-]+|font-family)\s*:\s*([^;}]+)/g)) {
856
- for (const fam of m[1].split(',')) {
857
- const name = fam.replace(/["']/g, '').trim();
858
- if (!name || /^var\(/.test(name)) continue;
859
- if (!isBound(name)) add('cod-16', d.file, i + 1, name, `Font family "${name}" binds to nothing installed: add its registering @fontsource import (or @font-face) to the emitted CSS, or fall back to a system generic.`);
860
- }
861
- }
862
- });
863
- }
864
- }
865
-
866
- // Bucket a 6-digit hex into a coarse hue family name for shade counting.
867
- function hexHueBucket(hex) {
868
- const h = expandHex(hex);
869
- const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
870
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
871
- if (max - min < 24) return 'grey';
872
- let hue;
873
- const d = max - min;
874
- if (max === r) hue = ((g - b) / d) % 6;
875
- else if (max === g) hue = (b - r) / d + 2;
876
- else hue = (r - g) / d + 4;
877
- hue = (hue * 60 + 360) % 360;
878
- if (hue < 20 || hue >= 340) return 'red';
879
- if (hue < 45) return 'orange';
880
- if (hue < 70) return 'yellow';
881
- if (hue < 165) return 'green';
882
- if (hue < 200) return 'teal';
883
- if (hue < 255) return 'blue';
884
- if (hue < 290) return 'violet';
885
- return 'pink';
886
- }
887
-
888
- // ---------------------------------------------------------------------------
889
- // cod-02 — lever-manifest drift check (D10), vocabulary from the design-tokens artifact
890
- // ---------------------------------------------------------------------------
891
-
892
- // Resolve a manifest argument (a design.tokens.json path or a directory holding one) to the tokens file, or null.
893
- function resolveTokensFile(path) {
894
- if (!path || !existsSync(path)) return null;
895
- let st;
896
- try { st = statSync(path); } catch { return null; }
897
- if (st.isDirectory()) {
898
- const candidate = join(path, 'design.tokens.json');
899
- return existsSync(candidate) ? candidate : null;
900
- }
901
- return path;
902
- }
903
-
904
- // Follow {path.to.token} alias strings within the artifact to the target token (hop-bounded against cycles).
905
- function resolveAlias(root, token) {
906
- let cur = token;
907
- for (let hops = 0; hops < 8 && cur && typeof cur.$value === 'string' && /^\{[^}]+\}$/.test(cur.$value.trim()); hops++) {
908
- let node = root;
909
- for (const seg of cur.$value.trim().slice(1, -1).split('.')) node = node && typeof node === 'object' ? node[seg] : undefined;
910
- if (!node || typeof node !== 'object') return cur;
911
- cur = node;
912
- }
913
- return cur;
914
- }
915
-
916
- // Pull a token's preserved source string from $extensions["com.nurix.design"].css, or null.
917
- function preservedCss(token) {
918
- const css = token && token.$extensions && token.$extensions['com.nurix.design'] && token.$extensions['com.nurix.design'].css;
919
- return typeof css === 'string' ? css : null;
920
- }
921
-
922
- // Load the drift-check vocabulary from a design-tokens artifact (a design.tokens.json path, or its directory):
923
- // scales come from the payload's lever-manifest; colors, font stacks, and icon stroke are read from the tokens
924
- // themselves (data/design-tokens/SCHEMA.md — token-valued allowlists are never restated in the manifest).
925
- // Each color enters in both spellings — the preserved .css string and the typed value's hex — so either passes
926
- // drift. Returns null when no parseable artifact is found; never runs the coverage gate.
927
- export function loadManifestVocab(manifestPath) {
928
- const file = resolveTokensFile(manifestPath);
929
- if (!file) return null;
930
- let root;
931
- try { root = JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
932
- if (!root || typeof root !== 'object') return null;
933
- const payload = (root.$extensions && root.$extensions['com.nurix.design']) || {};
934
- const manifest = payload['lever-manifest'] || {};
935
-
936
- const numSet = (arr) => (Array.isArray(arr) ? new Set(arr.map(Number).filter((n) => !Number.isNaN(n))) : null);
937
- const spacing = numSet(manifest['spacing-scale']);
938
- const radius = numSet(manifest['radius-scale']);
939
- const durations = numSet(manifest.motion && manifest.motion.durations);
940
-
941
- const colors = new Set();
942
- for (const token of Object.values(root.color || {})) {
943
- if (!token || typeof token !== 'object') continue;
944
- const resolved = resolveAlias(root, token);
945
- const css = preservedCss(token) || preservedCss(resolved);
946
- if (css) colors.add(normColor(css));
947
- const value = resolved && resolved.$value;
948
- if (value && typeof value === 'object' && typeof value.hex === 'string') colors.add(normColor(value.hex));
949
- }
950
-
951
- const fonts = new Set();
952
- // Add every family name in a fontFamily token's own stack (alias-valued tokens contribute nothing new).
953
- const addStack = (token) => {
954
- if (!token || !Array.isArray(token.$value)) return;
955
- for (const fam of token.$value) if (typeof fam === 'string' && fam.trim()) fonts.add(fam.trim().toLowerCase());
956
- };
957
- const primitive = root.primitive || {};
958
- addStack(primitive['font-sans']);
959
- addStack(primitive['font-mono']);
960
- for (const token of Object.values((root.typography && root.typography.family) || {})) addStack(token);
961
-
962
- const strokeToken = primitive['icon-stroke'] ? resolveAlias(root, primitive['icon-stroke']) : null;
963
- const iconStroke = strokeToken && typeof strokeToken.$value === 'number' ? strokeToken.$value : null;
964
-
965
- // The v2 elevation/wash vocabularies: every color literal inside a shadow composite's or a wash
966
- // gradient's preserved .css enters the color allowlist — both as the drift extractor will match it
967
- // in emitted CSS (the raw RE_COLOR_LITERAL slice) and as the layer's own typed hex — so consuming
968
- // the artifact's designed shadows/washes never reads as drift.
969
- const addExpressiveGroup = (group) => {
970
- for (const token of Object.values(group || {})) {
971
- if (!token || typeof token !== 'object') continue;
972
- const css = preservedCss(token);
973
- if (css) {
974
- for (const m of css.matchAll(RE_COLOR_LITERAL)) colors.add(normColor(m[0]));
975
- }
976
- const layers = Array.isArray(token.$value) ? token.$value : [];
977
- for (const layer of layers) {
978
- const c = layer && (layer.color || null);
979
- if (c && typeof c === 'object' && typeof c.hex === 'string') colors.add(normColor(c.hex));
980
- }
981
- }
982
- };
983
- addExpressiveGroup(root.elevation);
984
- addExpressiveGroup(root.wash);
985
-
986
- // Alpha-stripped base signatures of every manifest color, plus the emitter's fixed neutral
987
- // hairline base rgb(15 23 42) (the 2xs/xs shadow utilities the library leans on) — consulted
988
- // only for literals whose alpha slot rides var()/calc(), where exact normalization is defeated.
989
- const colorBases = new Set(['rgb:15,23,42']);
990
- for (const c of colors) {
991
- const b = colorBaseSig(c);
992
- if (b) colorBases.add(b);
993
- }
994
-
995
- return { colors, spacing, radius, durations, fonts, iconStroke, colorBases };
996
- }
997
-
998
- // Flag any color/spacing/radius/duration/font literal in the diff files outside the manifest vocabulary (cod-02).
999
- export function checkDrift(add, docs, vocab) {
1000
- for (const d of docs) {
1001
- if (d.ext === 'css' && vocab.colors.size) {
1002
- // token-definition files legitimately hold the manifest's own literals; only flag component-side drift
1003
- }
1004
- d.lines.forEach((line, i) => {
1005
- const isJsx = d.ext === 'tsx' || d.ext === 'ts' || d.ext === 'html';
1006
- // colors
1007
- if (vocab.colors.size) {
1008
- for (const m of line.matchAll(RE_COLOR_LITERAL)) {
1009
- if (vocab.colors.has(normColor(m[0]))) continue;
1010
- // A var()/calc() alpha slot truncates the regex match mid-function and defeats exact
1011
- // normalization; such intensity-riding literals match on their base channels instead.
1012
- if (/(?:var|calc)\(/.test(m[0]) && vocab.colorBases && vocab.colorBases.has(colorBaseSig(m[0]))) continue;
1013
- add('cod-02', d.file, i + 1, m[0], "Value drifts from the lever manifest; use a color already in the artifact's color tokens, not a fresh literal.");
1014
- }
1015
- }
1016
- // spacing (explicit only)
1017
- if (vocab.spacing && vocab.spacing.size) {
1018
- for (const m of line.matchAll(/(?:[pm][trblxy]?|gap(?:-[xy])?|space-[xy]|inset)-\[(\d*\.?\d+)(px|rem)\]/g)) {
1019
- const px = toPx(m[1], m[2]);
1020
- if (px != null && !vocab.spacing.has(px)) add('cod-02', d.file, i + 1, m[0], `Spacing ${px}px is off the manifest spacing-scale; snap to a listed step.`);
1021
- }
1022
- }
1023
- // radius (explicit only)
1024
- if (vocab.radius && vocab.radius.size) {
1025
- for (const m of line.matchAll(/rounded-\[(\d*\.?\d+)(px|rem)?\]|border-radius\s*:\s*(\d*\.?\d+)(px|rem)/g)) {
1026
- const px = m[1] ? toPx(m[1], m[2] || 'px') : toPx(m[3], m[4]);
1027
- if (px != null && !vocab.radius.has(px)) add('cod-02', d.file, i + 1, m[0], `Radius ${px}px is off the manifest radius-scale; use a listed radius.`);
1028
- }
1029
- }
1030
- // durations (explicit only)
1031
- if (vocab.durations && vocab.durations.size) {
1032
- for (const m of line.matchAll(/duration-\[(\d+)ms\]|duration-(\d{2,4})\b/g)) {
1033
- const ms = m[1] ? parseInt(m[1], 10) : parseInt(m[2], 10);
1034
- if (!vocab.durations.has(ms)) add('cod-02', d.file, i + 1, m[0], `Duration ${ms}ms is off the manifest motion.durations; use a listed duration.`);
1035
- }
1036
- }
1037
- // fonts (declared families)
1038
- if (isJsx && vocab.fonts.size) {
1039
- for (const m of line.matchAll(/font-\[["']?([A-Za-z][\w\s-]+)["']?\]/g)) {
1040
- const fam = m[1].split(',')[0].replace(/["']/g, '').trim().toLowerCase();
1041
- if (!vocab.fonts.has(fam)) add('cod-02', d.file, i + 1, m[0], `Font "${m[1]}" is not in the manifest fonts; use the declared sans/mono.`);
1042
- }
1043
- }
1044
- });
1045
- }
1046
- }
1047
-
1048
- // ---------------------------------------------------------------------------
1049
- // Orchestration + output formatting
1050
- // ---------------------------------------------------------------------------
1051
-
1052
- // Run the full engine (per-line, per-file, copy, cross-file, spacing, drift) over loaded docs; returns sorted findings.
1053
- export function checkDocs(docs, { vocab = null } = {}) {
1054
- const sink = makeSink();
1055
- for (const d of docs) {
1056
- d.lines.forEach((line, i) => checkLine(sink.add, d.file, d.ext, i + 1, line, d.text));
1057
- checkFile(sink.add, d.file, d.ext, d.text, d.lines);
1058
- checkCopy(sink.add, d.file, cleanComments(d.lines));
1059
- }
1060
- checkCrossFile(sink.add, docs);
1061
- checkSpacingScale(sink.add, docs);
1062
- checkFontBinding(sink.add, docs);
1063
- if (vocab) checkDrift(sink.add, docs, vocab);
1064
- // Stable sort: by file, then line, then rule id.
1065
- sink.list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.rule.localeCompare(b.rule));
1066
- return sink.list;
1067
- }
1068
-
1069
- // Convenience entry: read the passed file paths (and the optional design-tokens artifact) and run the full engine.
1070
- export function runChecks(files, { manifestPath = null, vocab = null } = {}) {
1071
- const loaded = vocab || (manifestPath ? loadManifestVocab(manifestPath) : null);
1072
- return checkDocs(loadDocs(files), { vocab: loaded });
1073
- }
1074
-
1075
- // Render findings as the fix-or-classify block (the caller prints it and decides the exit code).
1076
- export function formatFindings(findings, { color = false, label = 'machine checks' } = {}) {
1077
- const bold = (s) => (color ? `\x1b[1m${s}\x1b[0m` : s);
1078
- const red = (s) => (color ? `\x1b[31m${s}\x1b[0m` : s);
1079
- const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
1080
- const out = [];
1081
- out.push(bold(red(`${label}: ${findings.length} source violation${findings.length === 1 ? '' : 's'}`)));
1082
- out.push('');
1083
- for (const f of findings) {
1084
- out.push(`${bold(red('[' + f.rule + ']'))} ${f.file}:${f.line}`);
1085
- out.push(` ${dim('offending:')} ${f.excerpt}`);
1086
- out.push(` ${dim('fix:')} ${f.fix}`);
1087
- out.push('');
1088
- }
1089
- out.push(bold('Fix each now, or classify why the rule does not apply and record that classification'));
1090
- out.push(bold('in the decay ledger (docs/memory/design-decay.md) — never ignore silently.'));
1091
- return out.join('\n');
1092
- }