@nurix/etna 0.19.10 → 0.19.12

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