@nurix/etna 0.19.66 → 0.19.68

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 (234) hide show
  1. package/bin/etna.js +10 -1
  2. package/data/harness-index.json +1 -5
  3. package/data/harnesses/code/CLAUDE.md +2 -0
  4. package/data/harnesses/code/agents/maintenance.md +4 -1
  5. package/data/harnesses/code/harness.json +7 -2
  6. package/data/harnesses/code/hooks/awareness.mjs +69 -0
  7. package/data/harnesses/code/hooks/compliance.mjs +2 -14
  8. package/data/harnesses/code/hooks/lib/approved.mjs +30 -0
  9. package/data/harnesses/code/hooks/lib/frozen.mjs +3 -0
  10. package/data/harnesses/code/hooks/lib/ownership.mjs +3 -1
  11. package/data/harnesses/code/skills/documentation/references/frontmatter.md +1 -1
  12. package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +1 -1
  13. package/data/harnesses/code/skills/documentation/templates/Readme.md +2 -0
  14. package/data/harnesses/code/skills/documentation/templates/structure.md +1 -0
  15. package/data/harnesses/delivery/agents/maintenance.md +4 -1
  16. package/data/harnesses/delivery/harness.json +2 -2
  17. package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +1 -1
  18. package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +1 -1
  19. package/data/harnesses/delivery/skills/documentation/templates/Readme.md +2 -0
  20. package/data/harnesses/delivery/skills/documentation/templates/structure.md +1 -0
  21. package/data/harnesses/etna/agents/maintenance.md +4 -1
  22. package/data/harnesses/etna/harness.json +2 -2
  23. package/data/harnesses/etna/skills/documentation/references/frontmatter.md +1 -1
  24. package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +1 -1
  25. package/data/harnesses/etna/skills/documentation/templates/Readme.md +2 -0
  26. package/data/harnesses/etna/skills/documentation/templates/structure.md +1 -0
  27. package/lib/install/materialize.mjs +51 -57
  28. package/package.json +2 -2
  29. package/data/harnesses/apollo/.mcp.json +0 -8
  30. package/data/harnesses/apollo/.prettierignore +0 -29
  31. package/data/harnesses/apollo/.prettierrc.json +0 -3
  32. package/data/harnesses/apollo/.vscode/settings.json +0 -19
  33. package/data/harnesses/apollo/CLAUDE.md +0 -162
  34. package/data/harnesses/apollo/agents/bookkeeper.md +0 -28
  35. package/data/harnesses/apollo/agents/brainstorm.md +0 -144
  36. package/data/harnesses/apollo/agents/change-review.md +0 -251
  37. package/data/harnesses/apollo/agents/lld-mode.md +0 -58
  38. package/data/harnesses/apollo/agents/maintenance.md +0 -234
  39. package/data/harnesses/apollo/agents/performance-review.md +0 -223
  40. package/data/harnesses/apollo/agents/retro.md +0 -46
  41. package/data/harnesses/apollo/codex/agents/bookkeeper.toml +0 -26
  42. package/data/harnesses/apollo/codex/agents/brainstorm.toml +0 -142
  43. package/data/harnesses/apollo/codex/agents/change-review.toml +0 -249
  44. package/data/harnesses/apollo/codex/agents/lld-mode.toml +0 -52
  45. package/data/harnesses/apollo/codex/agents/maintenance.toml +0 -228
  46. package/data/harnesses/apollo/codex/agents/performance-review.toml +0 -222
  47. package/data/harnesses/apollo/codex/agents/retro.toml +0 -45
  48. package/data/harnesses/apollo/harness.json +0 -19
  49. package/data/harnesses/apollo/hooks/approved-packages.md +0 -44
  50. package/data/harnesses/apollo/hooks/awareness.mjs +0 -405
  51. package/data/harnesses/apollo/hooks/check-bookkeeping-clean.mjs +0 -19
  52. package/data/harnesses/apollo/hooks/check-changelog-presence.mjs +0 -57
  53. package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +0 -71
  54. package/data/harnesses/apollo/hooks/check-ownership.mjs +0 -22
  55. package/data/harnesses/apollo/hooks/compliance.mjs +0 -457
  56. package/data/harnesses/apollo/hooks/hooks.json +0 -23
  57. package/data/harnesses/apollo/hooks/lib/frozen.mjs +0 -58
  58. package/data/harnesses/apollo/hooks/lib/ownership.mjs +0 -242
  59. package/data/harnesses/apollo/hooks/lib/state.mjs +0 -170
  60. package/data/harnesses/apollo/rules/changelog.md +0 -27
  61. package/data/harnesses/apollo/rules/database.md +0 -34
  62. package/data/harnesses/apollo/rules/frontend.md +0 -30
  63. package/data/harnesses/apollo/rules/general.md +0 -40
  64. package/data/harnesses/apollo/rules/nomenclature.md +0 -30
  65. package/data/harnesses/apollo/rules/pnpm.md +0 -57
  66. package/data/harnesses/apollo/rules/testing.md +0 -64
  67. package/data/harnesses/apollo/skills/boundary/SKILL.md +0 -75
  68. package/data/harnesses/apollo/skills/boundary/references/index.md +0 -16
  69. package/data/harnesses/apollo/skills/boundary/references/mechanisms.md +0 -99
  70. package/data/harnesses/apollo/skills/boundary/references/packs/async-messaging.md +0 -81
  71. package/data/harnesses/apollo/skills/boundary/references/packs/auth.md +0 -93
  72. package/data/harnesses/apollo/skills/boundary/references/packs/build-config-runtime.md +0 -93
  73. package/data/harnesses/apollo/skills/boundary/references/packs/cloudflare.md +0 -93
  74. package/data/harnesses/apollo/skills/boundary/references/packs/frontend.md +0 -93
  75. package/data/harnesses/apollo/skills/boundary/references/packs/generic-tail.md +0 -55
  76. package/data/harnesses/apollo/skills/boundary/references/packs/persistence.md +0 -93
  77. package/data/harnesses/apollo/skills/boundary/references/packs/wire-api.md +0 -87
  78. package/data/harnesses/apollo/skills/brainstorm/README.md +0 -56
  79. package/data/harnesses/apollo/skills/brainstorm/SKILL.md +0 -173
  80. package/data/harnesses/apollo/skills/brainstorm/references/doctrine.md +0 -98
  81. package/data/harnesses/apollo/skills/brainstorm/references/harvest.md +0 -86
  82. package/data/harnesses/apollo/skills/brainstorm/references/session-schema.md +0 -186
  83. package/data/harnesses/apollo/skills/brainstorm/scripts/detector.mjs +0 -460
  84. package/data/harnesses/apollo/skills/design/SKILL.md +0 -121
  85. package/data/harnesses/apollo/skills/design/components/charts.md +0 -76
  86. package/data/harnesses/apollo/skills/design/components/collections.md +0 -132
  87. package/data/harnesses/apollo/skills/design/components/feedback.md +0 -47
  88. package/data/harnesses/apollo/skills/design/components/hooks.md +0 -35
  89. package/data/harnesses/apollo/skills/design/components/index.md +0 -143
  90. package/data/harnesses/apollo/skills/design/components/inputs.md +0 -327
  91. package/data/harnesses/apollo/skills/design/components/layout.md +0 -52
  92. package/data/harnesses/apollo/skills/design/components/media.md +0 -66
  93. package/data/harnesses/apollo/skills/design/components/motion.md +0 -82
  94. package/data/harnesses/apollo/skills/design/components/navigation.md +0 -29
  95. package/data/harnesses/apollo/skills/design/components/overlays.md +0 -72
  96. package/data/harnesses/apollo/skills/design/components/setup.md +0 -40
  97. package/data/harnesses/apollo/skills/design/components/typography.md +0 -46
  98. package/data/harnesses/apollo/skills/design/components/utils.md +0 -37
  99. package/data/harnesses/apollo/skills/design/data/blueprints/browse.md +0 -38
  100. package/data/harnesses/apollo/skills/design/data/blueprints/creation-flow.md +0 -38
  101. package/data/harnesses/apollo/skills/design/data/blueprints/dashboard.md +0 -38
  102. package/data/harnesses/apollo/skills/design/data/blueprints/grid.md +0 -38
  103. package/data/harnesses/apollo/skills/design/data/blueprints/queue.md +0 -38
  104. package/data/harnesses/apollo/skills/design/data/blueprints/record-detail.md +0 -38
  105. package/data/harnesses/apollo/skills/design/data/blueprints/settings.md +0 -38
  106. package/data/harnesses/apollo/skills/design/data/charts/census.csv +0 -58
  107. package/data/harnesses/apollo/skills/design/data/checklists/browse.md +0 -21
  108. package/data/harnesses/apollo/skills/design/data/checklists/creation-flow.md +0 -20
  109. package/data/harnesses/apollo/skills/design/data/checklists/dashboard.md +0 -21
  110. package/data/harnesses/apollo/skills/design/data/checklists/grid.md +0 -21
  111. package/data/harnesses/apollo/skills/design/data/checklists/queue.md +0 -21
  112. package/data/harnesses/apollo/skills/design/data/checklists/record-detail.md +0 -21
  113. package/data/harnesses/apollo/skills/design/data/checklists/settings.md +0 -21
  114. package/data/harnesses/apollo/skills/design/data/chrome/board-column.md +0 -68
  115. package/data/harnesses/apollo/skills/design/data/chrome/data-table.md +0 -63
  116. package/data/harnesses/apollo/skills/design/data/chrome/side-panel.md +0 -60
  117. package/data/harnesses/apollo/skills/design/data/chrome/sidebar.md +0 -78
  118. package/data/harnesses/apollo/skills/design/data/chrome/stat-cards.md +0 -62
  119. package/data/harnesses/apollo/skills/design/data/chrome/topbar.md +0 -65
  120. package/data/harnesses/apollo/skills/design/data/elevation/ant-design-directional-shadow.md +0 -19
  121. package/data/harnesses/apollo/skills/design/data/elevation/apple-liquid-glass-material.md +0 -18
  122. package/data/harnesses/apollo/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +0 -20
  123. package/data/harnesses/apollo/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +0 -20
  124. package/data/harnesses/apollo/skills/design/data/elevation/github-primer-sparing-shadow.md +0 -19
  125. package/data/harnesses/apollo/skills/design/data/elevation/ibm-carbon-layer-and-border.md +0 -19
  126. package/data/harnesses/apollo/skills/design/data/elevation/linear-dark-surface-ladder.md +0 -17
  127. package/data/harnesses/apollo/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +0 -19
  128. package/data/harnesses/apollo/skills/design/data/elevation/material-3-tonal-elevation.md +0 -21
  129. package/data/harnesses/apollo/skills/design/data/elevation/neobrutalist-hard-offset.md +0 -19
  130. package/data/harnesses/apollo/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +0 -18
  131. package/data/harnesses/apollo/skills/design/data/elevation/notion-warm-minimal-hairline.md +0 -17
  132. package/data/harnesses/apollo/skills/design/data/elevation/polaris-elevation-inset-bevel.md +0 -21
  133. package/data/harnesses/apollo/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +0 -19
  134. package/data/harnesses/apollo/skills/design/data/elevation/shadcn-neutral-derived-radius.md +0 -19
  135. package/data/harnesses/apollo/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +0 -18
  136. package/data/harnesses/apollo/skills/design/data/elevation/tailwind-default-utility-scale.md +0 -19
  137. package/data/harnesses/apollo/skills/design/data/elevation/vercel-geist-shadow-as-border.md +0 -20
  138. package/data/harnesses/apollo/skills/design/data/iconography.md +0 -199
  139. package/data/harnesses/apollo/skills/design/data/motion/accordion-expand.md +0 -18
  140. package/data/harnesses/apollo/skills/design/data/motion/destructive-hold-confirm.md +0 -18
  141. package/data/harnesses/apollo/skills/design/data/motion/drag-dismiss-momentum.md +0 -18
  142. package/data/harnesses/apollo/skills/design/data/motion/empty-state-arrive.md +0 -18
  143. package/data/harnesses/apollo/skills/design/data/motion/hover-lift.md +0 -18
  144. package/data/harnesses/apollo/skills/design/data/motion/modal-center-arrive.md +0 -18
  145. package/data/harnesses/apollo/skills/design/data/motion/number-ticker-roll.md +0 -18
  146. package/data/harnesses/apollo/skills/design/data/motion/onboarding-bounce.md +0 -18
  147. package/data/harnesses/apollo/skills/design/data/motion/panel-slide.md +0 -18
  148. package/data/harnesses/apollo/skills/design/data/motion/press-feedback.md +0 -18
  149. package/data/harnesses/apollo/skills/design/data/motion/progress-linear-loop.md +0 -18
  150. package/data/harnesses/apollo/skills/design/data/motion/route-crossfade.md +0 -18
  151. package/data/harnesses/apollo/skills/design/data/motion/rubber-band-overscroll.md +0 -18
  152. package/data/harnesses/apollo/skills/design/data/motion/skeleton-shimmer.md +0 -18
  153. package/data/harnesses/apollo/skills/design/data/motion/stagger-arrive.md +0 -18
  154. package/data/harnesses/apollo/skills/design/data/motion/toast-enter-stack.md +0 -18
  155. package/data/harnesses/apollo/skills/design/data/motion/tooltip-chain-instant.md +0 -18
  156. package/data/harnesses/apollo/skills/design/data/motion/trigger-anchored-reveal.md +0 -18
  157. package/data/harnesses/apollo/skills/design/data/tells.csv +0 -34
  158. package/data/harnesses/apollo/skills/design/data/voices/bright-collaborator.md +0 -29
  159. package/data/harnesses/apollo/skills/design/data/voices/direct-operator.md +0 -29
  160. package/data/harnesses/apollo/skills/design/data/voices/earnest-helper.md +0 -29
  161. package/data/harnesses/apollo/skills/design/data/voices/editorial-composed.md +0 -29
  162. package/data/harnesses/apollo/skills/design/data/voices/quiet-concierge.md +0 -29
  163. package/data/harnesses/apollo/skills/design/data/voices/warm-guide.md +0 -29
  164. package/data/harnesses/apollo/skills/design/references/compose.md +0 -23
  165. package/data/harnesses/apollo/skills/design/references/content.md +0 -28
  166. package/data/harnesses/apollo/skills/design/references/emission.md +0 -21
  167. package/data/harnesses/apollo/skills/design/references/existing-apps.md +0 -27
  168. package/data/harnesses/apollo/skills/design/references/foundations.md +0 -57
  169. package/data/harnesses/apollo/skills/design/references/intake.md +0 -40
  170. package/data/harnesses/apollo/skills/design/references/object-pass.md +0 -24
  171. package/data/harnesses/apollo/skills/design/references/ship-gate.md +0 -29
  172. package/data/harnesses/apollo/skills/design/references/ux-patterns/Readme.md +0 -195
  173. package/data/harnesses/apollo/skills/design/references/ux-patterns/ai-layer.md +0 -49
  174. package/data/harnesses/apollo/skills/design/references/ux-patterns/attributes-lifecycle.md +0 -43
  175. package/data/harnesses/apollo/skills/design/references/ux-patterns/container-choice.md +0 -44
  176. package/data/harnesses/apollo/skills/design/references/ux-patterns/creation-paths.md +0 -46
  177. package/data/harnesses/apollo/skills/design/references/ux-patterns/dashboards.md +0 -42
  178. package/data/harnesses/apollo/skills/design/references/ux-patterns/modes.md +0 -42
  179. package/data/harnesses/apollo/skills/design/references/ux-patterns/object-model.md +0 -46
  180. package/data/harnesses/apollo/skills/design/references/ux-patterns/page-archetype.md +0 -48
  181. package/data/harnesses/apollo/skills/design/references/ux-patterns/promotion-demotion.md +0 -37
  182. package/data/harnesses/apollo/skills/design/references/ux-patterns/settings.md +0 -42
  183. package/data/harnesses/apollo/skills/design/references/ux-patterns/standard-pages.md +0 -46
  184. package/data/harnesses/apollo/skills/design/references/ux-patterns/state-matrix.md +0 -41
  185. package/data/harnesses/apollo/skills/design/references/ux-patterns/table-mechanics.md +0 -48
  186. package/data/harnesses/apollo/skills/design/references/ux-patterns/verbs.md +0 -45
  187. package/data/harnesses/apollo/skills/design/references/ux-patterns/viewer-context.md +0 -40
  188. package/data/harnesses/apollo/skills/design/scripts/README-validator.md +0 -27
  189. package/data/harnesses/apollo/skills/design/scripts/select-chart.mjs +0 -622
  190. package/data/harnesses/apollo/skills/design/scripts/validate-design-md.mjs +0 -1198
  191. package/data/harnesses/apollo/skills/documentation/SKILL.md +0 -135
  192. package/data/harnesses/apollo/skills/documentation/references/examples.md +0 -279
  193. package/data/harnesses/apollo/skills/documentation/references/frontmatter.md +0 -44
  194. package/data/harnesses/apollo/skills/documentation/references/instruction-style.md +0 -50
  195. package/data/harnesses/apollo/skills/documentation/references/style-guide.md +0 -384
  196. package/data/harnesses/apollo/skills/documentation/references/untemplated.md +0 -38
  197. package/data/harnesses/apollo/skills/documentation/scripts/doc-lint.mjs +0 -140
  198. package/data/harnesses/apollo/skills/documentation/templates/Readme.md +0 -35
  199. package/data/harnesses/apollo/skills/documentation/templates/api-reference.md +0 -20
  200. package/data/harnesses/apollo/skills/documentation/templates/architecture-decision.md +0 -121
  201. package/data/harnesses/apollo/skills/documentation/templates/backlog.md +0 -33
  202. package/data/harnesses/apollo/skills/documentation/templates/behavioral-checklist.md +0 -81
  203. package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +0 -18
  204. package/data/harnesses/apollo/skills/documentation/templates/changelog-entry.md +0 -57
  205. package/data/harnesses/apollo/skills/documentation/templates/changelog-index.md +0 -41
  206. package/data/harnesses/apollo/skills/documentation/templates/docs-readme.md +0 -72
  207. package/data/harnesses/apollo/skills/documentation/templates/domain-model.md +0 -30
  208. package/data/harnesses/apollo/skills/documentation/templates/environment-registry.md +0 -15
  209. package/data/harnesses/apollo/skills/documentation/templates/feature.md +0 -150
  210. package/data/harnesses/apollo/skills/documentation/templates/glossary.md +0 -11
  211. package/data/harnesses/apollo/skills/documentation/templates/implementation-map.md +0 -42
  212. package/data/harnesses/apollo/skills/documentation/templates/journal.md +0 -13
  213. package/data/harnesses/apollo/skills/documentation/templates/memory-main.md +0 -25
  214. package/data/harnesses/apollo/skills/documentation/templates/migration-runbook.md +0 -34
  215. package/data/harnesses/apollo/skills/documentation/templates/non-goals.md +0 -11
  216. package/data/harnesses/apollo/skills/documentation/templates/operations-procedure.md +0 -36
  217. package/data/harnesses/apollo/skills/documentation/templates/plan.md +0 -88
  218. package/data/harnesses/apollo/skills/documentation/templates/protected-files.md +0 -37
  219. package/data/harnesses/apollo/skills/documentation/templates/root-readme.md +0 -104
  220. package/data/harnesses/apollo/skills/documentation/templates/security-overview.md +0 -27
  221. package/data/harnesses/apollo/skills/documentation/templates/structure.md +0 -65
  222. package/data/harnesses/apollo/skills/documentation/templates/system-overview.md +0 -32
  223. package/data/harnesses/apollo/skills/documentation/templates/testing-strategy.md +0 -29
  224. package/data/harnesses/apollo/skills/schema-design/SKILL.md +0 -141
  225. package/data/harnesses/apollo/skills/test/SKILL.md +0 -104
  226. package/data/harnesses/apollo/skills/testing/SKILL.md +0 -166
  227. package/data/harnesses/apollo/skills/workflow-mapper/SKILL.md +0 -451
  228. package/data/harnesses/apollo/skills/workflow-mapper/references/patterns.md +0 -214
  229. /package/data/harnesses/{apollo → code}/docs/README.md +0 -0
  230. /package/data/harnesses/{apollo → code}/docs/history/CHANGELOG.md +0 -0
  231. /package/data/harnesses/{apollo → code}/docs/history/backlog.md +0 -0
  232. /package/data/harnesses/{apollo → code}/docs/memory/main.md +0 -0
  233. /package/data/harnesses/{apollo → code}/docs/structure.md +0 -0
  234. /package/data/harnesses/{apollo → code}/rules/cloudflare.md +0 -0
@@ -1,1198 +0,0 @@
1
- // skills/design/scripts/validate-design-md.mjs - the design.md system's validator + token emitter (v2 schema)
2
- //
3
- // The entire mechanical apparatus of the design.md system (docs/features/design-skill/08 §The validator):
4
- // one stdlib-only script, three moments — library admission, build-time on the project fork, and the
5
- // ship gate's drift check. Vendors its own YAML-subset parser (frontmatter + the `yaml design-values`
6
- // block) and its own oklch → sRGB → WCAG-contrast math. FAIL closes; WARN never does.
7
- //
8
- // CLI:
9
- // node validate-design-md.mjs <file|folder> [--mode admission|build|ship] [--json] [--emit]
10
- //
11
- // <file> validate one design.md.
12
- // <folder> validate every .md in the folder tree (skipping SCHEMA.md) plus index.csv integrity (dmv-07)
13
- // and the tree-wide naming scan (dmv-09).
14
- // --mode admission and build run the same check set; ship additionally runs dmv-11 (drift:
15
- // re-emit and byte-compare against a design.tokens.css sitting beside the input file).
16
- // Default: build.
17
- // --json machine report to stdout: {file, fails:[{id,detail}], warns:[{id,detail}]}
18
- // (an array of those objects in folder mode; folder-level checks attach to the folder path).
19
- // --emit compile the token artifact from the design-values block to stdout (byte-stable across
20
- // runs; redirect to design.tokens.css). Requires a single file; validation runs first and
21
- // any FAIL suppresses the artifact (report goes to stderr). Warns go to stderr.
22
- //
23
- // Exit codes: 0 pass (warns allowed), 1 any FAIL.
24
- //
25
- // FAIL checks (stable IDs, 08 §The validator):
26
- // dmv-01 frontmatter parses (strict YAML subset: no tabs, no duplicate keys, fixed key set);
27
- // schema: design-md/v2; required keys per tier; id matches filename.
28
- // dmv-02 the 12 numbered sections present, in order, non-empty; one trailing `yaml design-values` block.
29
- // dmv-03 the design-values block parses as YAML.
30
- // dmv-04 coverage: all 36 color roles × both themes (values parseable); type ramp steps
31
- // display/h1/h2/body/small/caption/data each with size+line+weight+family; register-scope;
32
- // elevation (radius, hairline, shadow, dark-ladder); motion (duration, easing, entrance-never,
33
- // identity-moments); iconography (set, stroke, grid); charts (categorical, sequential,
34
- // diverging); layout (base).
35
- // dmv-05 contrast on every declared pairing, both themes (pair table below).
36
- // dmv-06 no image reference anywhere in the file (law 7 — the shipped skill carries no pixels).
37
- // dmv-07 (folder mode) index.csv integrity, both directions: every row's file exists; every package
38
- // file has exactly one row; no duplicate file column.
39
- // dmv-08 font families use registered names ("Inter Variable", not "Inter" — known-bare-name table
40
- // below) and each declares a fallback stack ending in a generic family.
41
- // dmv-09 naming hygiene: no file named claude.md / CLAUDE.md (any case).
42
- // dmv-10 (tier: project only) primitives present, exactly nurix|native; source-package present.
43
- // dmv-11 (--mode ship) drift: the token artifact recompiled from the design.md byte-matches the
44
- // design.tokens.css beside it.
45
- //
46
- // dmv-05 — the exact contrast pair table (both themes; WCAG 2.1 relative-luminance contrast; colors
47
- // resolved oklch → OKLab → linear sRGB, gamut-clipped to [0,1], gamma-encoded, then the WCAG formula;
48
- // alpha is ignored — pairs are assumed opaque):
49
- // body-text pairs — FAIL below 4.5:1:
50
- // foreground on background
51
- // foreground on card
52
- // foreground on muted
53
- // card-foreground on card
54
- // popover-foreground on popover
55
- // secondary-foreground on secondary
56
- // muted-foreground on muted
57
- // sidebar-foreground on sidebar
58
- // sidebar-accent-foreground on sidebar-accent
59
- // UI/fill pairs — FAIL below 3.0:1, WARN below 4.5:1 (reported under dmv-05):
60
- // primary-foreground on primary
61
- // accent-foreground on accent
62
- // destructive-foreground on destructive
63
- // sidebar-primary-foreground on sidebar-primary
64
- //
65
- // WARN checks (taste smells — flags for the eyes, thresholds inline from the 08 warn table):
66
- // dmw-01 background inside the anti-cream band (L 0.84–0.97, C < 0.06, hue 40–100).
67
- // dmw-02 neutral surfaces pure gray (C < 0.005) or over-tinted (C > 0.015); neutral set =
68
- // background, card, popover, secondary, muted, border, input, surface-raised, surface-overlay;
69
- // true white/black (L ≥ 0.99 or ≤ 0.03) exempt — chroma is unrepresentable at the gamut poles.
70
- // dmw-03 any shadow color pure black (L < 0.1, C < 0.01) above 0.15 alpha (untinted).
71
- // dmw-04 any entrance easing `ease-in` (keyword or ease-in-shaped cubic-bezier); any duration above
72
- // its element-class budget (A6): press/tap 150ms, fade/enter 250ms, dialog/popover 300ms,
73
- // page/route 350ms, panel/drawer 400ms, anything else 500ms.
74
- // dmw-05 display family granted a data register (cells, buttons, labels, …) in register-scope.
75
- // dmw-06 dark theme without a lightness ladder: surface-raised / surface-overlay not ≥ ~0.04 L
76
- // (implemented as ≥ 0.035) above the surface below, or ladder hue drifting > 15° from
77
- // background (checked only where chroma ≥ 0.01 — hue is noise near gray).
78
- // dmw-07 accent chroma delta between themes > 0.06 (identity likely not held across themes).
79
- //
80
- // Emitted artifact shape (deterministic, byte-stable): a `:root, :root[data-theme="light"]` block and a
81
- // `:root[data-theme="dark"]` block (specificity armor — both selectors reach 0,2,0 via the attribute
82
- // form, so a component library's later bare `:root` cannot silently defeat a theme). The light block
83
- // carries the 36 roles as --<role> plus every theme-invariant group: --font-display/sans/mono (registered
84
- // names quoted, declared fallback stacks appended), --text-<step>-size/-line/-weight (+ -family/-tracking/
85
- // -features where authored), --radius-*, --shadow-*, --duration-*, --ease-*, and the layout vars
86
- // (--space-base, --sidebar-width[-collapsed], --topbar-height, --side-panel-width[-wide],
87
- // --content-max-width; unmapped keys emit as --layout-<key>). The dark block carries the 36 dark roles.
88
-
89
- import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
90
- import { join, resolve, basename, dirname, relative, sep } from "node:path";
91
- import { pathToFileURL } from "node:url";
92
- import process from "node:process";
93
-
94
- // ---------------------------------------------------------------------------
95
- // The v2 schema constants
96
- // ---------------------------------------------------------------------------
97
-
98
- // The 36-role roster, verbatim from 08 §The value block.
99
- export const COLOR_ROLES = [
100
- "background", "foreground", "card", "card-foreground", "popover", "popover-foreground",
101
- "primary", "primary-foreground", "secondary", "secondary-foreground", "muted", "muted-foreground",
102
- "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "warning",
103
- "border", "input", "ring", "surface-raised", "surface-overlay",
104
- "chart-1", "chart-2", "chart-3", "chart-4", "chart-5",
105
- "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground",
106
- "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring",
107
- ];
108
-
109
- const SECTIONS = [
110
- "1 Visual Theme", "2 Color Roles", "3 Typography", "4 Component Stylings", "5 Layout",
111
- "6 Depth", "7 Motion", "8 Iconography & Charts", "9 Voice", "10 Do's & Don'ts",
112
- "11 Responsive", "12 Agent Prompt Guide",
113
- ];
114
-
115
- const RAMP_STEPS = ["display", "h1", "h2", "body", "small", "caption", "data"];
116
- const FONT_SLOTS = ["display", "sans", "mono"];
117
- const THEMES = ["light", "dark"];
118
- const TIERS = ["default", "variant", "brand", "project"];
119
- const FRONT_KEYS_BASE = ["schema", "id", "tier", "segments", "keywords", "sourced", "sources", "license"];
120
- const FRONT_KEYS_PROJECT = ["primitives", "source-package"];
121
-
122
- const BODY_PAIRS = [
123
- ["foreground", "background"], ["foreground", "card"], ["foreground", "muted"],
124
- ["card-foreground", "card"], ["popover-foreground", "popover"],
125
- ["secondary-foreground", "secondary"], ["muted-foreground", "muted"],
126
- ["sidebar-foreground", "sidebar"], ["sidebar-accent-foreground", "sidebar-accent"],
127
- ];
128
- const UI_PAIRS = [
129
- ["primary-foreground", "primary"], ["accent-foreground", "accent"],
130
- ["destructive-foreground", "destructive"], ["sidebar-primary-foreground", "sidebar-primary"],
131
- ];
132
-
133
- const NEUTRAL_SURFACE_ROLES = [
134
- "background", "card", "popover", "secondary", "muted", "border", "input",
135
- "surface-raised", "surface-overlay",
136
- ];
137
-
138
- // Bare name → registered family, for the variable-first families the skill derives from
139
- // @fontsource-variable/* (registered as "<Family> Variable"). Extend as the library grows.
140
- const BARE_FONT_NAMES = {
141
- "inter": "Inter Variable", "fraunces": "Fraunces Variable", "manrope": "Manrope Variable",
142
- "sora": "Sora Variable", "outfit": "Outfit Variable", "epilogue": "Epilogue Variable",
143
- "karla": "Karla Variable", "lora": "Lora Variable", "newsreader": "Newsreader Variable",
144
- "space grotesk": "Space Grotesk Variable", "work sans": "Work Sans Variable",
145
- "instrument sans": "Instrument Sans Variable", "public sans": "Public Sans Variable",
146
- "plus jakarta sans": "Plus Jakarta Sans Variable", "figtree": "Figtree Variable",
147
- "schibsted grotesk": "Schibsted Grotesk Variable", "bricolage grotesque": "Bricolage Grotesque Variable",
148
- "source sans 3": "Source Sans 3 Variable", "dm sans": "DM Sans Variable", "geist": "Geist Variable",
149
- };
150
-
151
- const GENERIC_FAMILIES = new Set([
152
- "serif", "sans-serif", "monospace", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace",
153
- "ui-rounded", "cursive", "fantasy", "math", "emoji", "fangsong",
154
- ]);
155
-
156
- // A6 element-class duration budgets (ms), matched against the duration's name; first hit wins.
157
- const DURATION_BUDGETS = [
158
- [/press|tap|click|toggle|hover|instant/i, 150],
159
- [/fade|arrive|appear|enter|reveal/i, 250],
160
- [/panel|drawer|sheet|slide/i, 400],
161
- [/dialog|modal|popover|overlay|menu|tooltip/i, 300],
162
- [/page|route|view/i, 350],
163
- ];
164
- const DURATION_BUDGET_DEFAULT = 500;
165
-
166
- const DATA_REGISTER_WORDS = new Set([
167
- "data", "cell", "cells", "table", "tables", "row", "rows", "button", "buttons", "label", "labels",
168
- "control", "controls", "chrome", "form", "forms", "input", "inputs", "id", "ids",
169
- "timestamp", "timestamps", "numeral", "numerals", "badge", "badges", "chip", "chips",
170
- ]);
171
-
172
- // --emit names for layout keys; arrays map positionally; unmapped keys fall back to --layout-<key>[-n].
173
- const LAYOUT_EMIT = {
174
- "base": ["--space-base"],
175
- "sidebar": ["--sidebar-width", "--sidebar-width-collapsed"],
176
- "topbar": ["--topbar-height"],
177
- "side-panel": ["--side-panel-width", "--side-panel-width-wide"],
178
- "content-max": ["--content-max-width"],
179
- };
180
-
181
- // ---------------------------------------------------------------------------
182
- // Vendored YAML-subset parser (block maps/seqs by indentation; single-line flow
183
- // maps/seqs; double/single-quoted and plain scalars; comments; no anchors,
184
- // no multi-line scalars, no tabs, no duplicate keys)
185
- // ---------------------------------------------------------------------------
186
-
187
- class YamlError extends Error {}
188
-
189
- const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
190
-
191
- export function parseYaml(src, label) {
192
- const rawLines = src.split(/\r?\n/);
193
- const lines = [];
194
- for (let i = 0; i < rawLines.length; i++) {
195
- const raw = rawLines[i];
196
- if (/^\s*$/.test(raw) || /^\s*#/.test(raw)) continue;
197
- if (/^ *\t/.test(raw)) throw new YamlError(`${label} line ${i + 1}: tabs are not allowed in indentation`);
198
- const indent = raw.match(/^ */)[0].length;
199
- lines.push({ indent, text: raw.slice(indent).replace(/\s+$/, ""), lineNo: i + 1 });
200
- }
201
- if (lines.length === 0) return {};
202
- const ctx = { lines, pos: 0, label };
203
- const value = parseBlock(ctx, lines[0].indent);
204
- if (ctx.pos < lines.length) {
205
- const l = ctx.lines[ctx.pos];
206
- throw new YamlError(`${label} line ${l.lineNo}: unexpected content "${l.text}"`);
207
- }
208
- return value;
209
- }
210
-
211
- function parseBlock(ctx, indent) {
212
- const first = ctx.lines[ctx.pos];
213
- if (first.text === "-" || first.text.startsWith("- ")) return parseBlockSeq(ctx, indent);
214
- return parseBlockMap(ctx, indent);
215
- }
216
-
217
- function parseBlockMap(ctx, indent) {
218
- const obj = {};
219
- while (ctx.pos < ctx.lines.length) {
220
- const line = ctx.lines[ctx.pos];
221
- if (line.indent < indent) break;
222
- if (line.indent > indent) throw new YamlError(`${ctx.label} line ${line.lineNo}: unexpected indent`);
223
- if (line.text === "-" || line.text.startsWith("- ")) break;
224
- const m = matchKey(line.text);
225
- if (!m) throw new YamlError(`${ctx.label} line ${line.lineNo}: expected "key: value"`);
226
- if (Object.prototype.hasOwnProperty.call(obj, m.key)) {
227
- throw new YamlError(`${ctx.label} line ${line.lineNo}: duplicate key "${m.key}"`);
228
- }
229
- ctx.pos++;
230
- if (m.rest === "") {
231
- const next = ctx.lines[ctx.pos];
232
- obj[m.key] = next && next.indent > indent ? parseBlock(ctx, next.indent) : null;
233
- } else {
234
- obj[m.key] = parseInlineValue(m.rest, `${ctx.label} line ${line.lineNo}`);
235
- }
236
- }
237
- return obj;
238
- }
239
-
240
- function parseBlockSeq(ctx, indent) {
241
- const arr = [];
242
- while (ctx.pos < ctx.lines.length) {
243
- const line = ctx.lines[ctx.pos];
244
- if (line.indent < indent) break;
245
- if (line.indent > indent) throw new YamlError(`${ctx.label} line ${line.lineNo}: unexpected indent`);
246
- if (!(line.text === "-" || line.text.startsWith("- "))) break;
247
- const rest = line.text === "-" ? "" : line.text.slice(2).trim();
248
- ctx.pos++;
249
- if (rest === "") {
250
- const next = ctx.lines[ctx.pos];
251
- arr.push(next && next.indent > indent ? parseBlock(ctx, next.indent) : null);
252
- } else {
253
- arr.push(parseInlineValue(rest, `${ctx.label} line ${line.lineNo}`));
254
- }
255
- }
256
- return arr;
257
- }
258
-
259
- // Splits "key: rest" at the first ':' followed by a space or end-of-line.
260
- function matchKey(text) {
261
- let key, restStart;
262
- if (text[0] === '"' || text[0] === "'") {
263
- const q = readQuoted(text, 0, "key");
264
- if (text[q.end] !== ":") return null;
265
- key = q.value;
266
- restStart = q.end + 1;
267
- } else {
268
- let idx = -1;
269
- for (let i = 0; i < text.length; i++) {
270
- if (text[i] === ":" && (i + 1 === text.length || text[i + 1] === " ")) { idx = i; break; }
271
- }
272
- if (idx === -1) return null;
273
- key = text.slice(0, idx).trim();
274
- restStart = idx + 1;
275
- }
276
- if (!key) return null;
277
- return { key, rest: text.slice(restStart).trim() };
278
- }
279
-
280
- function parseInlineValue(str, where) {
281
- const s = str.trim();
282
- if (s === "") return null;
283
- const c = s[0];
284
- if (c === "{" || c === "[") {
285
- const r = parseFlow(s, 0, where);
286
- ensureOnlyCommentAfter(s, r.end, where);
287
- return r.value;
288
- }
289
- if (c === '"' || c === "'") {
290
- const q = readQuoted(s, 0, where);
291
- ensureOnlyCommentAfter(s, q.end, where);
292
- return q.value;
293
- }
294
- let cut = s.length;
295
- for (let i = 1; i < s.length; i++) {
296
- if (s[i] === "#" && /\s/.test(s[i - 1])) { cut = i; break; }
297
- }
298
- return convertScalar(s.slice(0, cut).trim());
299
- }
300
-
301
- function ensureOnlyCommentAfter(s, idx, where) {
302
- const rest = s.slice(idx).trim();
303
- if (rest !== "" && !rest.startsWith("#")) throw new YamlError(`${where}: unexpected trailing content "${rest}"`);
304
- }
305
-
306
- function readQuoted(s, pos, where) {
307
- const q = s[pos];
308
- let out = "";
309
- let i = pos + 1;
310
- while (i < s.length) {
311
- const c = s[i];
312
- if (q === '"' && c === "\\") {
313
- const n = s[i + 1];
314
- out += n === "n" ? "\n" : n === "t" ? "\t" : (n ?? "");
315
- i += 2;
316
- continue;
317
- }
318
- if (c === q) {
319
- if (q === "'" && s[i + 1] === "'") { out += "'"; i += 2; continue; }
320
- return { value: out, end: i + 1 };
321
- }
322
- out += c;
323
- i++;
324
- }
325
- throw new YamlError(`${where}: unterminated quoted string`);
326
- }
327
-
328
- const skipWs = (s, pos) => { while (pos < s.length && s[pos] === " ") pos++; return pos; };
329
-
330
- function parseFlow(s, pos, where) {
331
- pos = skipWs(s, pos);
332
- const c = s[pos];
333
- if (c === "{") {
334
- const obj = {};
335
- pos = skipWs(s, pos + 1);
336
- if (s[pos] === "}") return { value: obj, end: pos + 1 };
337
- for (;;) {
338
- let key;
339
- if (s[pos] === '"' || s[pos] === "'") {
340
- const q = readQuoted(s, pos, where);
341
- key = q.value;
342
- pos = skipWs(s, q.end);
343
- } else {
344
- let j = pos;
345
- while (j < s.length && s[j] !== ":") j++;
346
- if (j >= s.length) throw new YamlError(`${where}: missing ":" in flow map`);
347
- key = s.slice(pos, j).trim();
348
- pos = j;
349
- }
350
- if (s[pos] !== ":") throw new YamlError(`${where}: expected ":" after flow key "${key}"`);
351
- if (Object.prototype.hasOwnProperty.call(obj, key)) throw new YamlError(`${where}: duplicate key "${key}"`);
352
- const r = parseFlowValue(s, pos + 1, where);
353
- obj[key] = r.value;
354
- pos = skipWs(s, r.end);
355
- if (s[pos] === ",") { pos = skipWs(s, pos + 1); continue; }
356
- if (s[pos] === "}") return { value: obj, end: pos + 1 };
357
- throw new YamlError(`${where}: expected "," or "}" in flow map`);
358
- }
359
- }
360
- if (c === "[") {
361
- const arr = [];
362
- pos = skipWs(s, pos + 1);
363
- if (s[pos] === "]") return { value: arr, end: pos + 1 };
364
- for (;;) {
365
- const r = parseFlowValue(s, pos, where);
366
- arr.push(r.value);
367
- pos = skipWs(s, r.end);
368
- if (s[pos] === ",") { pos = skipWs(s, pos + 1); continue; }
369
- if (s[pos] === "]") return { value: arr, end: pos + 1 };
370
- throw new YamlError(`${where}: expected "," or "]" in flow sequence`);
371
- }
372
- }
373
- throw new YamlError(`${where}: expected a flow collection`);
374
- }
375
-
376
- function parseFlowValue(s, pos, where) {
377
- pos = skipWs(s, pos);
378
- const c = s[pos];
379
- if (c === "{" || c === "[") return parseFlow(s, pos, where);
380
- if (c === '"' || c === "'") return readQuoted(s, pos, where);
381
- // Plain scalar in flow context: runs to the next , } ] outside parentheses,
382
- // so unquoted oklch(…)/cubic-bezier(…) values survive intact.
383
- let depth = 0;
384
- let i = pos;
385
- for (; i < s.length; i++) {
386
- const ch = s[i];
387
- if (ch === "(") depth++;
388
- else if (ch === ")") depth = Math.max(0, depth - 1);
389
- else if (depth === 0 && (ch === "," || ch === "}" || ch === "]")) break;
390
- }
391
- return { value: convertScalar(s.slice(pos, i).trim()), end: i };
392
- }
393
-
394
- function convertScalar(raw) {
395
- if (raw === "" || raw === "null" || raw === "~") return null;
396
- if (raw === "true") return true;
397
- if (raw === "false") return false;
398
- if (/^-?(\d+\.?\d*|\.\d+)$/.test(raw)) return Number(raw);
399
- return raw;
400
- }
401
-
402
- // ---------------------------------------------------------------------------
403
- // Vendored color math: oklch → OKLab → linear sRGB (gamut-clipped) → WCAG contrast
404
- // ---------------------------------------------------------------------------
405
-
406
- function oklchToSrgb(L, C, Hdeg) {
407
- const h = (Hdeg * Math.PI) / 180;
408
- const a = C * Math.cos(h);
409
- const b = C * Math.sin(h);
410
- const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
411
- const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
412
- const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
413
- const l = l_ ** 3;
414
- const m = m_ ** 3;
415
- const s = s_ ** 3;
416
- const lin = [
417
- 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
418
- -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
419
- -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
420
- ];
421
- return lin.map((ch) => {
422
- const v = Math.min(1, Math.max(0, ch)); // gamut clip
423
- return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
424
- });
425
- }
426
-
427
- function srgbToOklch(srgb) {
428
- const [r, g, bl] = srgb.map((c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)));
429
- const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * bl);
430
- const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * bl);
431
- const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * bl);
432
- const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
433
- const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
434
- const b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
435
- const C = Math.hypot(a, b);
436
- let H = (Math.atan2(b, a) * 180) / Math.PI;
437
- H = ((H % 360) + 360) % 360;
438
- return { L, C, H };
439
- }
440
-
441
- // Parses an oklch(L C H [/ a]) or #hex color into { L, C, H, alpha, srgb }; null if unparseable.
442
- export function parseColor(str) {
443
- if (typeof str !== "string") return null;
444
- const s = str.trim();
445
- let m = s.match(/^oklch\(\s*(\d*\.?\d+)(%)?\s+(\d*\.?\d+)\s+(-?\d*\.?\d+)(?:deg)?\s*(?:\/\s*(\d*\.?\d+)(%)?\s*)?\)$/i);
446
- if (m) {
447
- const L = parseFloat(m[1]) / (m[2] ? 100 : 1);
448
- const C = parseFloat(m[3]);
449
- const H = ((parseFloat(m[4]) % 360) + 360) % 360;
450
- const alpha = m[5] == null ? 1 : parseFloat(m[5]) / (m[6] ? 100 : 1);
451
- return { L, C, H, alpha, srgb: oklchToSrgb(L, C, H) };
452
- }
453
- m = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
454
- if (m) {
455
- let hex = m[1];
456
- if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
457
- const srgb = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
458
- const alpha = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
459
- const { L, C, H } = srgbToOklch(srgb);
460
- return { L, C, H, alpha, srgb };
461
- }
462
- return null;
463
- }
464
-
465
- function relativeLuminance(srgb) {
466
- const [r, g, b] = srgb.map((c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)));
467
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
468
- }
469
-
470
- export function contrastRatio(c1, c2) {
471
- const y1 = relativeLuminance(c1.srgb);
472
- const y2 = relativeLuminance(c2.srgb);
473
- const hi = Math.max(y1, y2);
474
- const lo = Math.min(y1, y2);
475
- return (hi + 0.05) / (lo + 0.05);
476
- }
477
-
478
- const hueDistance = (a, b) => { const d = Math.abs(a - b) % 360; return Math.min(d, 360 - d); };
479
- const fmtNum = (n) => String(Math.round(n * 10000) / 10000);
480
-
481
- // ---------------------------------------------------------------------------
482
- // Report plumbing
483
- // ---------------------------------------------------------------------------
484
-
485
- const fail = (report, id, detail) => report.fails.push({ id, detail });
486
- const warn = (report, id, detail) => report.warns.push({ id, detail });
487
-
488
- // ---------------------------------------------------------------------------
489
- // Checks
490
- // ---------------------------------------------------------------------------
491
-
492
- // dmv-06 — no image reference anywhere in the raw file.
493
- function checkNoImages(raw, report) {
494
- const patterns = [
495
- [/!\[/, 'markdown image syntax "!["'],
496
- [/<img\b/i, "an <img> tag"],
497
- [/data:image\//i, "a data:image URI"],
498
- [/\b[\w./%-]+\.(png|jpe?g|gif|webp|avif|bmp|ico|tiff?|svg)\b/i, "an image file reference"],
499
- ];
500
- for (const [re, what] of patterns) {
501
- const m = raw.match(re);
502
- if (m) fail(report, "dmv-06", `file contains ${what} ("${m[0].slice(0, 60)}") — the shipped skill carries no pixels (law 7)`);
503
- }
504
- }
505
-
506
- // Splits "---\n…\n---\n<body>"; null when the file has no leading frontmatter fence.
507
- function extractFrontmatter(raw) {
508
- const lines = raw.split(/\r?\n/);
509
- if (lines[0] !== "---") return null;
510
- for (let i = 1; i < lines.length; i++) {
511
- if (lines[i] === "---") {
512
- return { yaml: lines.slice(1, i).join("\n"), body: lines.slice(i + 1).join("\n") };
513
- }
514
- }
515
- return null;
516
- }
517
-
518
- // dmv-01 + dmv-10 — frontmatter schema, per-tier key set, id/filename match.
519
- function checkFrontmatter(front, filePath, report) {
520
- if (!isObj(front)) { fail(report, "dmv-01", "frontmatter is not a mapping"); return; }
521
- if (front.schema !== "design-md/v2") fail(report, "dmv-01", `schema must be "design-md/v2", got "${front.schema}"`);
522
- const tier = front.tier;
523
- if (!TIERS.includes(tier)) fail(report, "dmv-01", `tier must be one of ${TIERS.join("|")}, got "${tier}"`);
524
- const expectedId = basename(filePath).replace(/\.md$/i, "");
525
- if (typeof front.id !== "string" || !front.id.trim()) fail(report, "dmv-01", "id missing");
526
- else if (front.id !== expectedId) fail(report, "dmv-01", `id "${front.id}" does not match filename "${expectedId}"`);
527
- for (const k of ["segments", "keywords"]) {
528
- if (!Array.isArray(front[k]) || front[k].length === 0) fail(report, "dmv-01", `${k} must be a non-empty list`);
529
- }
530
- if (typeof front.sourced !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(front.sourced)) {
531
- fail(report, "dmv-01", `sourced must be a YYYY-MM-DD date, got "${front.sourced}"`);
532
- }
533
- if (typeof front.sources !== "string" || !front.sources.trim()) fail(report, "dmv-01", "sources missing");
534
- if (front.license !== "open" && front.license !== "licensed") {
535
- fail(report, "dmv-01", `license must be open|licensed, got "${front.license}"`);
536
- }
537
- const allowed = new Set(tier === "project" ? [...FRONT_KEYS_BASE, ...FRONT_KEYS_PROJECT] : FRONT_KEYS_BASE);
538
- for (const k of Object.keys(front)) {
539
- if (allowed.has(k)) continue;
540
- fail(report, "dmv-01", FRONT_KEYS_PROJECT.includes(k)
541
- ? `key "${k}" is allowed on tier: project only`
542
- : `unknown frontmatter key "${k}"`);
543
- }
544
- if (tier === "project") {
545
- if (front.primitives !== "nurix" && front.primitives !== "native") {
546
- fail(report, "dmv-10", `primitives must be exactly "nurix" or "native", got "${front.primitives ?? "(missing)"}"`);
547
- }
548
- if (typeof front["source-package"] !== "string" || !front["source-package"].trim()) {
549
- fail(report, "dmv-10", 'source-package missing (provenance; "authored" for escape-hatch one-offs)');
550
- }
551
- }
552
- }
553
-
554
- const normHeading = (s) => s.replace(/[‘’]/g, "'").replace(/\s+/g, " ").trim().toLowerCase();
555
-
556
- // dmv-02 — 12 sections in order, non-empty, plus exactly one trailing design-values block.
557
- // Returns { valueBlock } — the block's raw YAML text, or null.
558
- function checkSections(body, report) {
559
- const lines = body.split(/\r?\n/);
560
- const headings = [];
561
- const dvBlocks = [];
562
- let inFence = false;
563
- let fenceIsDV = false;
564
- let dvStart = -1;
565
- let dvContent = [];
566
- for (let i = 0; i < lines.length; i++) {
567
- const line = lines[i];
568
- const fence = line.match(/^```(.*)$/);
569
- if (fence) {
570
- if (!inFence) {
571
- inFence = true;
572
- fenceIsDV = /^\s*yaml\s+design-values\s*$/.test(fence[1]);
573
- if (fenceIsDV) { dvStart = i; dvContent = []; }
574
- } else {
575
- if (fenceIsDV) dvBlocks.push({ start: dvStart, end: i, text: dvContent.join("\n") });
576
- inFence = false;
577
- fenceIsDV = false;
578
- }
579
- continue;
580
- }
581
- if (inFence) {
582
- if (fenceIsDV) dvContent.push(line);
583
- continue;
584
- }
585
- const h = line.match(/^##\s+(.*)$/);
586
- if (h) headings.push({ title: h[1].trim(), idx: i });
587
- }
588
- if (inFence) fail(report, "dmv-02", "unclosed code fence");
589
-
590
- const expected = SECTIONS.map(normHeading);
591
- const found = headings.map((h) => normHeading(h.title));
592
- for (let i = 0; i < expected.length; i++) {
593
- if (i >= found.length) { fail(report, "dmv-02", `section "${SECTIONS[i]}" missing`); continue; }
594
- if (found[i] !== expected[i]) {
595
- fail(report, "dmv-02", `expected section "${SECTIONS[i]}" at position ${i + 1}, found "${headings[i].title}"`);
596
- }
597
- }
598
- for (let i = expected.length; i < found.length; i++) {
599
- fail(report, "dmv-02", `unexpected extra section "${headings[i].title}"`);
600
- }
601
-
602
- const dv = dvBlocks.length ? dvBlocks[dvBlocks.length - 1] : null;
603
- for (let i = 0; i < headings.length && i < SECTIONS.length; i++) {
604
- const start = headings[i].idx + 1;
605
- let end = i + 1 < headings.length ? headings[i + 1].idx : lines.length;
606
- if (dv && dv.start >= start && dv.start < end) end = dv.start;
607
- if (!lines.slice(start, end).some((l) => /\S/.test(l))) {
608
- fail(report, "dmv-02", `section "${headings[i].title}" is empty`);
609
- }
610
- }
611
-
612
- if (dvBlocks.length === 0) {
613
- fail(report, "dmv-02", "trailing `yaml design-values` block missing");
614
- return { valueBlock: null };
615
- }
616
- if (dvBlocks.length > 1) fail(report, "dmv-02", `exactly one design-values block allowed, found ${dvBlocks.length}`);
617
- if (lines.slice(dv.end + 1).some((l) => /\S/.test(l))) {
618
- fail(report, "dmv-02", "design-values block must be the last content in the file");
619
- }
620
- if (headings.length >= SECTIONS.length && dv.start < headings[SECTIONS.length - 1].idx) {
621
- fail(report, "dmv-02", "design-values block must come after section 12");
622
- }
623
- return { valueBlock: dv.text };
624
- }
625
-
626
- // dmv-04 — 100% coverage of the value block, mechanically.
627
- function checkCoverage(values, report) {
628
- const missing = (path) => fail(report, "dmv-04", `${path} missing`);
629
-
630
- const color = values.color;
631
- if (!isObj(color)) missing("color");
632
- else {
633
- for (const theme of THEMES) {
634
- const roles = color[theme];
635
- if (!isObj(roles)) { missing(`color.${theme}`); continue; }
636
- const absent = COLOR_ROLES.filter((r) => roles[r] == null);
637
- if (absent.length) fail(report, "dmv-04", `color.${theme} missing ${absent.length}/36 roles: ${absent.join(", ")}`);
638
- const bad = COLOR_ROLES.filter((r) => roles[r] != null && !parseColor(roles[r]));
639
- if (bad.length) fail(report, "dmv-04", `color.${theme} unparseable color values: ${bad.join(", ")}`);
640
- }
641
- }
642
-
643
- const typo = values.typography;
644
- if (!isObj(typo)) missing("typography");
645
- else {
646
- for (const slot of FONT_SLOTS) if (!isObj(typo[slot])) missing(`typography.${slot}`);
647
- if (!isObj(typo.ramp)) missing("typography.ramp");
648
- else {
649
- for (const step of RAMP_STEPS) {
650
- const r = typo.ramp[step];
651
- if (!isObj(r)) { missing(`typography.ramp.${step}`); continue; }
652
- for (const k of ["size", "line", "weight"]) if (r[k] == null) missing(`typography.ramp.${step}.${k}`);
653
- if (r.family == null) missing(`typography.ramp.${step}.family`);
654
- else if (!FONT_SLOTS.includes(r.family)) {
655
- fail(report, "dmv-04", `typography.ramp.${step}.family must be one of ${FONT_SLOTS.join("|")}, got "${r.family}"`);
656
- }
657
- }
658
- }
659
- const scope = typo["register-scope"];
660
- if (!isObj(scope) || Object.keys(scope).length === 0) missing("typography.register-scope");
661
- }
662
-
663
- const elev = values.elevation;
664
- if (!isObj(elev)) missing("elevation");
665
- else {
666
- if (!isObj(elev.radius) || Object.keys(elev.radius).length === 0) missing("elevation.radius");
667
- if (elev.hairline == null) missing("elevation.hairline");
668
- if (!isObj(elev.shadow) || Object.keys(elev.shadow).length === 0) missing("elevation.shadow");
669
- if (!isObj(elev["dark-ladder"]) || Object.keys(elev["dark-ladder"]).length === 0) missing("elevation.dark-ladder");
670
- }
671
-
672
- const motion = values.motion;
673
- if (!isObj(motion)) missing("motion");
674
- else {
675
- if (!isObj(motion.duration) || Object.keys(motion.duration).length === 0) missing("motion.duration");
676
- if (!isObj(motion.easing) || Object.keys(motion.easing).length === 0) missing("motion.easing");
677
- if (motion["entrance-never"] == null) missing("motion.entrance-never");
678
- if (!Array.isArray(motion["identity-moments"]) || motion["identity-moments"].length === 0) missing("motion.identity-moments");
679
- }
680
-
681
- const icon = values.iconography;
682
- if (!isObj(icon)) missing("iconography");
683
- else for (const k of ["set", "stroke", "grid"]) if (icon[k] == null) missing(`iconography.${k}`);
684
-
685
- const charts = values.charts;
686
- if (!isObj(charts)) missing("charts");
687
- else {
688
- if (!Array.isArray(charts.categorical) || charts.categorical.length === 0) missing("charts.categorical");
689
- if (!isObj(charts.sequential)) missing("charts.sequential");
690
- if (!isObj(charts.diverging)) missing("charts.diverging");
691
- }
692
-
693
- const layout = values.layout;
694
- if (!isObj(layout) || Object.keys(layout).length === 0) missing("layout");
695
- else if (layout.base == null) missing("layout.base");
696
- }
697
-
698
- // dmv-05 — contrast on every declared pairing, both themes (pair table in the header comment).
699
- function checkContrast(values, report) {
700
- const color = isObj(values.color) ? values.color : {};
701
- for (const theme of THEMES) {
702
- const roles = isObj(color[theme]) ? color[theme] : null;
703
- if (!roles) continue;
704
- const get = (r) => (roles[r] != null ? parseColor(roles[r]) : null);
705
- for (const [fg, bg] of BODY_PAIRS) {
706
- const a = get(fg);
707
- const b = get(bg);
708
- if (!a || !b) continue; // dmv-04 already reported the gap
709
- const ratio = contrastRatio(a, b);
710
- if (ratio < 4.5) fail(report, "dmv-05", `${theme}: ${fg} on ${bg} = ${ratio.toFixed(2)}:1 (body text needs ≥ 4.5:1)`);
711
- }
712
- for (const [fg, bg] of UI_PAIRS) {
713
- const a = get(fg);
714
- const b = get(bg);
715
- if (!a || !b) continue;
716
- const ratio = contrastRatio(a, b);
717
- if (ratio < 3) fail(report, "dmv-05", `${theme}: ${fg} on ${bg} = ${ratio.toFixed(2)}:1 (UI fill needs ≥ 3:1)`);
718
- else if (ratio < 4.5) warn(report, "dmv-05", `${theme}: ${fg} on ${bg} = ${ratio.toFixed(2)}:1 — passes the 3:1 UI floor but sits below 4.5:1 body-text comfort`);
719
- }
720
- }
721
- }
722
-
723
- // dmv-08 — registered family names + fallback stacks.
724
- function checkFonts(values, report) {
725
- const typo = isObj(values.typography) ? values.typography : {};
726
- for (const slot of FONT_SLOTS) {
727
- const f = typo[slot];
728
- if (!isObj(f)) continue; // dmv-04 already reported the gap
729
- if (typeof f.family !== "string" || !f.family.trim()) { fail(report, "dmv-08", `typography.${slot}: family missing`); continue; }
730
- const registered = BARE_FONT_NAMES[f.family.trim().toLowerCase()];
731
- if (registered) fail(report, "dmv-08", `typography.${slot}: "${f.family}" is the bare name — the registered family is "${registered}"`);
732
- if (typeof f.fallback !== "string" || !f.fallback.trim()) { fail(report, "dmv-08", `typography.${slot}: fallback stack missing`); continue; }
733
- const parts = f.fallback.split(",").map((p) => p.trim()).filter(Boolean);
734
- const last = parts[parts.length - 1];
735
- if (!last || !GENERIC_FAMILIES.has(last.toLowerCase())) {
736
- fail(report, "dmv-08", `typography.${slot}: fallback stack must end in a generic family (serif, sans-serif, monospace, …), ends with "${last}"`);
737
- }
738
- }
739
- }
740
-
741
- // dmw-01..07 — the taste-smell warns.
742
- function runWarnChecks(values, report) {
743
- const color = isObj(values.color) ? values.color : {};
744
- const raw = {};
745
- const parsed = {};
746
- for (const t of THEMES) {
747
- raw[t] = isObj(color[t]) ? color[t] : {};
748
- parsed[t] = {};
749
- for (const [k, v] of Object.entries(raw[t])) {
750
- const c = parseColor(v);
751
- if (c) parsed[t][k] = c;
752
- }
753
- }
754
-
755
- // dmw-01 — the anti-cream band.
756
- for (const t of THEMES) {
757
- const bg = parsed[t].background;
758
- if (bg && bg.L >= 0.84 && bg.L <= 0.97 && bg.C < 0.06 && bg.H >= 40 && bg.H <= 100) {
759
- warn(report, "dmw-01", `${t} background ${raw[t].background} sits in the anti-cream band (L 0.84–0.97, C < 0.06, hue 40–100) — the AI-default tell`);
760
- }
761
- }
762
-
763
- // dmw-02 — neutral chroma bounds (0.005–0.015 toward the brand hue).
764
- for (const t of THEMES) {
765
- const pure = [];
766
- const over = [];
767
- for (const role of NEUTRAL_SURFACE_ROLES) {
768
- const c = parsed[t][role];
769
- if (!c) continue;
770
- if (c.L >= 0.99 || c.L <= 0.03) continue; // gamut poles: chroma unrepresentable at true white/black
771
- if (c.C < 0.005) pure.push(`${role} (C ${fmtNum(c.C)})`);
772
- else if (c.C > 0.015) over.push(`${role} (C ${fmtNum(c.C)})`);
773
- }
774
- if (pure.length) warn(report, "dmw-02", `${t} neutrals pure gray (C < 0.005; tint 0.005–0.015 toward the brand hue): ${pure.join(", ")}`);
775
- if (over.length) warn(report, "dmw-02", `${t} neutrals over-tinted (C > 0.015): ${over.join(", ")}`);
776
- }
777
-
778
- // dmw-03 — pure-black shadows above 0.15 alpha.
779
- const elev = isObj(values.elevation) ? values.elevation : {};
780
- if (isObj(elev.shadow)) {
781
- for (const [name, val] of Object.entries(elev.shadow)) {
782
- if (typeof val !== "string") continue;
783
- const colors = val.match(/oklch\([^)]*\)|rgba?\([^)]*\)|#[0-9a-f]{3,8}\b|\bblack\b/gi) || [];
784
- for (const cstr of colors) {
785
- let L, C, alpha;
786
- if (/^black$/i.test(cstr)) { L = 0; C = 0; alpha = 1; }
787
- else if (/^rgba?/i.test(cstr)) {
788
- const nums = (cstr.match(/[\d.]+%?/g) || []).map((n) => (n.endsWith("%") ? parseFloat(n) / 100 : parseFloat(n)));
789
- if (nums.length < 3) continue;
790
- const srgb = nums.slice(0, 3).map((n) => (n > 1 ? n / 255 : n));
791
- ({ L, C } = srgbToOklch(srgb));
792
- alpha = nums.length > 3 ? nums[3] : 1;
793
- } else {
794
- const c = parseColor(cstr);
795
- if (!c) continue;
796
- ({ L, C, alpha } = c);
797
- }
798
- if (C < 0.01 && L < 0.1 && alpha > 0.15) {
799
- warn(report, "dmw-03", `shadow.${name} uses untinted black at alpha ${fmtNum(alpha)} (> 0.15) — tint the shadow toward the brand hue`);
800
- }
801
- }
802
- }
803
- }
804
-
805
- // dmw-04 — ease-in entrances; durations over their element-class budgets.
806
- const motion = isObj(values.motion) ? values.motion : {};
807
- if (isObj(motion.duration)) {
808
- for (const [name, val] of Object.entries(motion.duration)) {
809
- if (typeof val !== "string" && typeof val !== "number") continue;
810
- const m = String(val).match(/^(\d+(?:\.\d+)?)\s*(ms|s)?$/);
811
- if (!m) continue;
812
- const ms = parseFloat(m[1]) * (m[2] === "s" ? 1000 : 1);
813
- let budget = DURATION_BUDGET_DEFAULT;
814
- for (const [re, b] of DURATION_BUDGETS) if (re.test(name)) { budget = b; break; }
815
- if (ms > budget) warn(report, "dmw-04", `duration.${name} = ${fmtNum(ms)}ms exceeds its element-class budget of ${budget}ms (A6)`);
816
- }
817
- }
818
- if (isObj(motion.easing)) {
819
- for (const [name, val] of Object.entries(motion.easing)) {
820
- if (/exit|out|leave|dismiss|close/i.test(name)) continue; // exits may accelerate
821
- if (typeof val !== "string") continue;
822
- if (val.trim() === "ease-in") {
823
- warn(report, "dmw-04", `easing.${name} is \`ease-in\` — entrances never ease-in`);
824
- continue;
825
- }
826
- const bez = val.match(/cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)/);
827
- if (bez) {
828
- const [x1, y1, x2, y2] = bez.slice(1, 5).map(Number);
829
- if (y1 <= 0.05 && x1 >= 0.3 && x2 >= 0.9 && y2 >= 0.9) {
830
- warn(report, "dmw-04", `easing.${name} = ${val} is ease-in-shaped (accelerating) — entrances never ease-in`);
831
- }
832
- }
833
- }
834
- }
835
-
836
- // dmw-05 — display face granted a data register.
837
- const typo = isObj(values.typography) ? values.typography : {};
838
- const scope = isObj(typo["register-scope"]) ? typo["register-scope"] : {};
839
- if (Array.isArray(scope.display)) {
840
- for (const grant of scope.display) {
841
- const words = String(grant).toLowerCase().split(/[^a-z]+/).filter(Boolean);
842
- if (words.some((w) => DATA_REGISTER_WORDS.has(w))) {
843
- warn(report, "dmw-05", `register-scope grants the display face "${grant}" — a data register (display never touches cells, buttons, labels, or form chrome)`);
844
- }
845
- }
846
- }
847
-
848
- // dmw-06 — the dark lightness ladder.
849
- const d = parsed.dark;
850
- if (d.background && d["surface-raised"] && d["surface-overlay"]) {
851
- const steps = [
852
- ["surface-raised", "background", d.background, d["surface-raised"]],
853
- ["surface-overlay", "surface-raised", d["surface-raised"], d["surface-overlay"]],
854
- ];
855
- for (const [name, belowName, below, above] of steps) {
856
- const delta = above.L - below.L;
857
- if (delta < 0.035) {
858
- warn(report, "dmw-06", `dark ${name} is only ${fmtNum(delta)} L above ${belowName} — the dark ladder needs ≥ ~0.04 L per step`);
859
- }
860
- }
861
- for (const name of ["surface-raised", "surface-overlay"]) {
862
- const c = d[name];
863
- if (c.C >= 0.01 && d.background.C >= 0.01) {
864
- const drift = hueDistance(c.H, d.background.H);
865
- if (drift > 15) warn(report, "dmw-06", `dark ${name} hue drifts ${fmtNum(drift)}° from background — hold the hue on the ladder`);
866
- }
867
- }
868
- }
869
-
870
- // dmw-07 — accent identity across themes.
871
- const aL = parsed.light.accent;
872
- const aD = parsed.dark.accent;
873
- if (aL && aD) {
874
- const delta = Math.abs(aL.C - aD.C);
875
- if (delta > 0.06) warn(report, "dmw-07", `accent chroma delta between themes is ${fmtNum(delta)} (> 0.06) — identity likely not held across themes`);
876
- }
877
- }
878
-
879
- // ---------------------------------------------------------------------------
880
- // Emission — the derived token artifact (deterministic, byte-stable)
881
- // ---------------------------------------------------------------------------
882
-
883
- function req(values, path) {
884
- let cur = values;
885
- for (const k of path) {
886
- if (cur == null || cur[k] == null) throw new Error(`cannot emit: ${path.join(".")} missing`);
887
- cur = cur[k];
888
- }
889
- return cur;
890
- }
891
-
892
- const cssValue = (v) => String(v);
893
-
894
- export function emitTokens(front, values) {
895
- const id = front && typeof front.id === "string" ? front.id : "unknown";
896
- const out = [];
897
- out.push(`/* design.tokens.css — compiled from ${id} (design-md/v2) by validate-design-md.mjs — derived artifact: edit the design.md, never this file */`);
898
- out.push("");
899
- out.push(':root, :root[data-theme="light"] {');
900
- out.push(" /* color roles */");
901
- for (const role of COLOR_ROLES) out.push(` --${role}: ${cssValue(req(values, ["color", "light", role]))};`);
902
- out.push("");
903
- out.push(" /* typography */");
904
- for (const slot of FONT_SLOTS) {
905
- const f = req(values, ["typography", slot]);
906
- if (f.family == null || f.fallback == null) throw new Error(`cannot emit: typography.${slot} family/fallback missing`);
907
- out.push(` --font-${slot}: "${f.family}", ${f.fallback};`);
908
- }
909
- for (const step of RAMP_STEPS) {
910
- const r = req(values, ["typography", "ramp", step]);
911
- out.push(` --text-${step}-size: ${cssValue(req(r, ["size"]))};`);
912
- out.push(` --text-${step}-line: ${cssValue(req(r, ["line"]))};`);
913
- out.push(` --text-${step}-weight: ${cssValue(req(r, ["weight"]))};`);
914
- if (r.family != null) out.push(` --text-${step}-family: var(--font-${r.family});`);
915
- if (r.tracking != null) out.push(` --text-${step}-tracking: ${cssValue(r.tracking)};`);
916
- if (Array.isArray(r.features) && r.features.length) {
917
- out.push(` --text-${step}-features: ${r.features.map((x) => `"${x}"`).join(", ")};`);
918
- }
919
- }
920
- out.push("");
921
- out.push(" /* elevation */");
922
- for (const [k, v] of Object.entries(req(values, ["elevation", "radius"]))) out.push(` --radius-${k}: ${cssValue(v)};`);
923
- for (const [k, v] of Object.entries(req(values, ["elevation", "shadow"]))) out.push(` --shadow-${k}: ${cssValue(v)};`);
924
- out.push("");
925
- out.push(" /* motion */");
926
- for (const [k, v] of Object.entries(req(values, ["motion", "duration"]))) out.push(` --duration-${k}: ${cssValue(v)};`);
927
- for (const [k, v] of Object.entries(req(values, ["motion", "easing"]))) out.push(` --ease-${k}: ${cssValue(v)};`);
928
- out.push("");
929
- out.push(" /* layout */");
930
- for (const [k, v] of Object.entries(req(values, ["layout"]))) {
931
- const names = LAYOUT_EMIT[k];
932
- if (Array.isArray(v)) {
933
- for (let i = 0; i < v.length; i++) {
934
- out.push(` ${(names && names[i]) || `--layout-${k}-${i + 1}`}: ${cssValue(v[i])};`);
935
- }
936
- } else {
937
- out.push(` ${(names && names[0]) || `--layout-${k}`}: ${cssValue(v)};`);
938
- }
939
- }
940
- out.push("}");
941
- out.push("");
942
- out.push(':root[data-theme="dark"] {');
943
- out.push(" /* color roles */");
944
- for (const role of COLOR_ROLES) out.push(` --${role}: ${cssValue(req(values, ["color", "dark", role]))};`);
945
- out.push("}");
946
- return out.join("\n") + "\n";
947
- }
948
-
949
- function firstDiff(a, b) {
950
- const n = Math.min(a.length, b.length);
951
- for (let i = 0; i < n; i++) if (a[i] !== b[i]) return i;
952
- return n;
953
- }
954
-
955
- // ---------------------------------------------------------------------------
956
- // File / folder runners
957
- // ---------------------------------------------------------------------------
958
-
959
- export function validateFile(filePath, mode = "build", opts = {}) {
960
- const report = { file: filePath, fails: [], warns: [] };
961
- let raw;
962
- try {
963
- raw = readFileSync(filePath, "utf8");
964
- } catch (e) {
965
- fail(report, "dmv-01", `unreadable: ${e.message}`);
966
- return report;
967
- }
968
-
969
- if (opts.checkOwnName !== false && basename(filePath).toLowerCase() === "claude.md") {
970
- fail(report, "dmv-09", "file is named claude.md — an agent runtime auto-loads it as instructions (naming hygiene)");
971
- }
972
-
973
- checkNoImages(raw, report);
974
-
975
- const fm = extractFrontmatter(raw);
976
- let front = null;
977
- if (!fm) {
978
- fail(report, "dmv-01", "missing YAML frontmatter (--- … ---) at the top of the file");
979
- } else {
980
- try {
981
- front = parseYaml(fm.yaml, "frontmatter");
982
- } catch (e) {
983
- fail(report, "dmv-01", e.message);
984
- }
985
- if (front) checkFrontmatter(front, filePath, report);
986
- }
987
-
988
- const body = fm ? fm.body : raw;
989
- const { valueBlock } = checkSections(body, report);
990
-
991
- let values = null;
992
- if (valueBlock != null) {
993
- try {
994
- values = parseYaml(valueBlock, "design-values");
995
- if (!isObj(values)) { fail(report, "dmv-03", "design-values block is not a mapping"); values = null; }
996
- } catch (e) {
997
- fail(report, "dmv-03", e.message);
998
- }
999
- }
1000
-
1001
- if (values) {
1002
- checkCoverage(values, report);
1003
- checkContrast(values, report);
1004
- checkFonts(values, report);
1005
- runWarnChecks(values, report);
1006
- try {
1007
- report._artifact = emitTokens(front, values);
1008
- } catch {
1009
- report._artifact = null;
1010
- }
1011
- }
1012
-
1013
- if (mode === "ship") {
1014
- const sibling = join(dirname(filePath), "design.tokens.css");
1015
- if (!report._artifact) {
1016
- fail(report, "dmv-11", "cannot recompile the token artifact (value block missing or incomplete)");
1017
- } else if (!existsSync(sibling)) {
1018
- fail(report, "dmv-11", `design.tokens.css not found beside ${basename(filePath)} — the derived artifact must ship with its source`);
1019
- } else {
1020
- const disk = readFileSync(sibling);
1021
- const fresh = Buffer.from(report._artifact, "utf8");
1022
- if (!disk.equals(fresh)) {
1023
- fail(report, "dmv-11", `drift: design.tokens.css does not byte-match the recompiled artifact (first divergence at byte ${firstDiff(disk, fresh)})`);
1024
- }
1025
- }
1026
- }
1027
-
1028
- return report;
1029
- }
1030
-
1031
- function walk(dir) {
1032
- const out = [];
1033
- const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
1034
- for (const e of entries) {
1035
- if (e.name.startsWith(".")) continue;
1036
- const p = join(dir, e.name);
1037
- if (e.isDirectory()) out.push(...walk(p));
1038
- else if (e.isFile()) out.push(p);
1039
- }
1040
- return out;
1041
- }
1042
-
1043
- const toRel = (root, p) => relative(root, p).split(sep).join("/");
1044
-
1045
- // dmv-07 — index.csv integrity, both directions.
1046
- function checkIndex(root, packageFiles, report) {
1047
- const indexPath = join(root, "index.csv");
1048
- if (!existsSync(indexPath)) {
1049
- fail(report, "dmv-07", "index.csv not found at the folder root");
1050
- return;
1051
- }
1052
- let rows;
1053
- try {
1054
- rows = parseCsv(readFileSync(indexPath, "utf8"));
1055
- } catch (e) {
1056
- fail(report, "dmv-07", `index.csv malformed: ${e.message}`);
1057
- return;
1058
- }
1059
- const HEADER = ["file", "tier", "segments", "keywords", "reads-as"];
1060
- if (!rows.length || rows[0].join(",") !== HEADER.join(",")) {
1061
- fail(report, "dmv-07", `index.csv header must be "${HEADER.join(",")}", got "${(rows[0] || []).join(",")}"`);
1062
- return;
1063
- }
1064
- const seen = new Map();
1065
- for (let n = 1; n < rows.length; n++) {
1066
- const row = rows[n];
1067
- if (row.length !== HEADER.length) {
1068
- fail(report, "dmv-07", `index.csv row ${n + 1}: expected ${HEADER.length} fields, got ${row.length}`);
1069
- continue;
1070
- }
1071
- const file = row[0];
1072
- if (seen.has(file)) fail(report, "dmv-07", `index.csv duplicate file "${file}" (rows ${seen.get(file) + 1} and ${n + 1})`);
1073
- else seen.set(file, n);
1074
- if (!existsSync(resolve(root, file))) fail(report, "dmv-07", `index.csv row ${n + 1}: file "${file}" does not exist`);
1075
- }
1076
- for (const rel of packageFiles) {
1077
- if (!seen.has(rel)) fail(report, "dmv-07", `package file "${rel}" has no index.csv row`);
1078
- }
1079
- }
1080
-
1081
- function parseCsv(text) {
1082
- const rows = [];
1083
- let row = [];
1084
- let field = "";
1085
- let inQ = false;
1086
- let i = 0;
1087
- while (i < text.length) {
1088
- const c = text[i];
1089
- if (inQ) {
1090
- if (c === '"') {
1091
- if (text[i + 1] === '"') { field += '"'; i += 2; continue; }
1092
- inQ = false; i++; continue;
1093
- }
1094
- field += c; i++; continue;
1095
- }
1096
- if (c === '"') { inQ = true; i++; continue; }
1097
- if (c === ",") { row.push(field); field = ""; i++; continue; }
1098
- if (c === "\n" || c === "\r") {
1099
- if (c === "\r" && text[i + 1] === "\n") i++;
1100
- row.push(field); field = ""; rows.push(row); row = []; i++; continue;
1101
- }
1102
- field += c; i++;
1103
- }
1104
- if (inQ) throw new Error("unterminated quoted field");
1105
- if (field !== "" || row.length) { row.push(field); rows.push(row); }
1106
- return rows.filter((r) => !(r.length === 1 && r[0].trim() === ""));
1107
- }
1108
-
1109
- export function validateFolder(root, mode = "build") {
1110
- const reports = [];
1111
- const allFiles = walk(root);
1112
- const mdFiles = allFiles.filter((f) => f.toLowerCase().endsWith(".md") && basename(f).toLowerCase() !== "schema.md");
1113
- for (const f of mdFiles) reports.push(validateFile(f, mode, { checkOwnName: false }));
1114
-
1115
- const folderReport = { file: root, fails: [], warns: [] };
1116
- for (const f of allFiles) {
1117
- if (basename(f).toLowerCase() === "claude.md") {
1118
- fail(folderReport, "dmv-09", `"${toRel(root, f)}" is named claude.md — an agent runtime auto-loads it as instructions (naming hygiene)`);
1119
- }
1120
- }
1121
- checkIndex(root, mdFiles.map((f) => toRel(root, f)), folderReport);
1122
- reports.push(folderReport);
1123
- return reports;
1124
- }
1125
-
1126
- // ---------------------------------------------------------------------------
1127
- // CLI
1128
- // ---------------------------------------------------------------------------
1129
-
1130
- function usage(stream) {
1131
- stream.write("usage: node validate-design-md.mjs <file|folder> [--mode admission|build|ship] [--json] [--emit]\n");
1132
- }
1133
-
1134
- function printHuman(reports, stream) {
1135
- let fails = 0;
1136
- let warns = 0;
1137
- for (const r of reports) {
1138
- stream.write(`${r.file}\n`);
1139
- for (const f of r.fails) stream.write(` FAIL ${f.id} — ${f.detail}\n`);
1140
- for (const w of r.warns) stream.write(` WARN ${w.id} — ${w.detail}\n`);
1141
- if (!r.fails.length && !r.warns.length) stream.write(" ok\n");
1142
- fails += r.fails.length;
1143
- warns += r.warns.length;
1144
- }
1145
- stream.write(`\n${fails ? "FAIL" : "PASS"} — ${reports.length} target(s), ${fails} fail(s), ${warns} warn(s)\n`);
1146
- }
1147
-
1148
- const toJsonReport = (r) => ({ file: r.file, fails: r.fails, warns: r.warns });
1149
-
1150
- function main() {
1151
- const args = process.argv.slice(2);
1152
- let target = null;
1153
- let mode = "build";
1154
- let json = false;
1155
- let emit = false;
1156
- for (let i = 0; i < args.length; i++) {
1157
- const a = args[i];
1158
- if (a === "--mode") {
1159
- mode = args[++i];
1160
- } else if (a === "--json") json = true;
1161
- else if (a === "--emit") emit = true;
1162
- else if (a === "--help" || a === "-h") { usage(process.stdout); process.exit(0); }
1163
- else if (!a.startsWith("--") && target === null) target = a;
1164
- else { process.stderr.write(`unknown argument: ${a}\n`); usage(process.stderr); process.exit(1); }
1165
- }
1166
- if (!target) { usage(process.stderr); process.exit(1); }
1167
- if (!["admission", "build", "ship"].includes(mode)) {
1168
- process.stderr.write(`--mode must be admission|build|ship, got "${mode}"\n`);
1169
- process.exit(1);
1170
- }
1171
- const abs = resolve(target);
1172
- if (!existsSync(abs)) { process.stderr.write(`no such file or folder: ${abs}\n`); process.exit(1); }
1173
- const isDir = statSync(abs).isDirectory();
1174
- if (emit && isDir) { process.stderr.write("--emit requires a single design.md file\n"); process.exit(1); }
1175
-
1176
- const reports = isDir ? validateFolder(abs, mode) : [validateFile(abs, mode, { checkOwnName: true })];
1177
- const anyFail = reports.some((r) => r.fails.length > 0);
1178
-
1179
- if (emit) {
1180
- if (anyFail || !reports[0]._artifact) {
1181
- printHuman(reports, process.stderr);
1182
- process.exit(1);
1183
- }
1184
- process.stdout.write(reports[0]._artifact);
1185
- if (reports[0].warns.length) printHuman(reports, process.stderr);
1186
- process.exit(0);
1187
- }
1188
-
1189
- if (json) {
1190
- const payload = isDir ? reports.map(toJsonReport) : toJsonReport(reports[0]);
1191
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
1192
- } else {
1193
- printHuman(reports, process.stdout);
1194
- }
1195
- process.exit(anyFail ? 1 : 0);
1196
- }
1197
-
1198
- if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main();