quiver-cli 1.3.1 → 2.0.0

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 (407) hide show
  1. package/README.md +197 -239
  2. package/dist/cli.js +3163 -2040
  3. package/package.json +10 -11
  4. package/template/.agents/AGENTS.md +0 -26
  5. package/template/.agents/commands/cp.md +0 -108
  6. package/template/.agents/commands/next-setup.md +0 -1064
  7. package/template/.agents/commands/review.md +0 -68
  8. package/template/.agents/commands/terraform-readme.md +0 -38
  9. package/template/.agents/config.json +0 -80
  10. package/template/.agents/plugins/opencode/rtk.ts +0 -34
  11. package/template/.agents/skills/agent-browser/SKILL.md +0 -52
  12. package/template/.agents/skills/apps/skybridge/SKILL.md +0 -52
  13. package/template/.agents/skills/code/cleanup/SKILL.md +0 -26
  14. package/template/.agents/skills/code/improve/SKILL.md +0 -122
  15. package/template/.agents/skills/code/improve/references/audit-playbook.md +0 -130
  16. package/template/.agents/skills/code/improve/references/closing-the-loop.md +0 -96
  17. package/template/.agents/skills/code/improve/references/plan-template.md +0 -197
  18. package/template/.agents/skills/code/vercel-react-best-practices/AGENTS.md +0 -3810
  19. package/template/.agents/skills/code/vercel-react-best-practices/README.md +0 -123
  20. package/template/.agents/skills/code/vercel-react-best-practices/SKILL.md +0 -149
  21. package/template/.agents/skills/code/vercel-react-best-practices/metadata.json +0 -15
  22. package/template/.agents/skills/code/vercel-react-best-practices/rules/_sections.md +0 -46
  23. package/template/.agents/skills/code/vercel-react-best-practices/rules/_template.md +0 -28
  24. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-effect-event-deps.md +0 -56
  25. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  26. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-init-once.md +0 -42
  27. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-use-latest.md +0 -39
  28. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  29. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +0 -37
  30. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-defer-await.md +0 -82
  31. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-dependencies.md +0 -51
  32. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-parallel.md +0 -28
  33. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  34. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-analyzable-paths.md +0 -63
  35. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -60
  36. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  37. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  38. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  39. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  40. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  41. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  42. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  43. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  44. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -107
  45. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  46. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  47. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  48. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  49. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  50. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-flatmap-filter.md +0 -60
  51. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  52. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  53. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
  54. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  55. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-request-idle-callback.md +0 -105
  56. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  57. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  58. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  59. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  60. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  61. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  62. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  63. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  64. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +0 -30
  65. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-resource-hints.md +0 -85
  66. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-script-defer-async.md +0 -68
  67. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  68. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-usetransition-loading.md +0 -75
  69. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  70. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  71. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +0 -40
  72. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  73. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  74. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  75. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +0 -38
  76. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  77. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +0 -45
  78. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-no-inline-components.md +0 -82
  79. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -35
  80. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +0 -64
  81. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  82. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-deferred-value.md +0 -59
  83. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +0 -73
  84. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  85. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-auth-actions.md +0 -96
  86. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  87. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  88. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-dedup-props.md +0 -65
  89. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-hoist-static-io.md +0 -149
  90. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-no-shared-module-state.md +0 -50
  91. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  92. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +0 -34
  93. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-serialization.md +0 -38
  94. package/template/.agents/skills/data/prisma-cli/SKILL.md +0 -265
  95. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +0 -27
  96. package/template/.agents/skills/data/prisma-cli/references/complete.md +0 -22
  97. package/template/.agents/skills/data/prisma-cli/references/db-execute.md +0 -78
  98. package/template/.agents/skills/data/prisma-cli/references/db-pull.md +0 -185
  99. package/template/.agents/skills/data/prisma-cli/references/db-push.md +0 -150
  100. package/template/.agents/skills/data/prisma-cli/references/db-seed.md +0 -188
  101. package/template/.agents/skills/data/prisma-cli/references/debug.md +0 -46
  102. package/template/.agents/skills/data/prisma-cli/references/dev.md +0 -157
  103. package/template/.agents/skills/data/prisma-cli/references/format.md +0 -48
  104. package/template/.agents/skills/data/prisma-cli/references/generate.md +0 -173
  105. package/template/.agents/skills/data/prisma-cli/references/init.md +0 -139
  106. package/template/.agents/skills/data/prisma-cli/references/mcp.md +0 -39
  107. package/template/.agents/skills/data/prisma-cli/references/migrate-deploy.md +0 -127
  108. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +0 -145
  109. package/template/.agents/skills/data/prisma-cli/references/migrate-diff.md +0 -89
  110. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +0 -80
  111. package/template/.agents/skills/data/prisma-cli/references/migrate-resolve.md +0 -57
  112. package/template/.agents/skills/data/prisma-cli/references/migrate-status.md +0 -65
  113. package/template/.agents/skills/data/prisma-cli/references/studio.md +0 -137
  114. package/template/.agents/skills/data/prisma-cli/references/validate.md +0 -53
  115. package/template/.agents/skills/data/prisma-client-api/SKILL.md +0 -216
  116. package/template/.agents/skills/data/prisma-client-api/references/client-methods.md +0 -223
  117. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +0 -221
  118. package/template/.agents/skills/data/prisma-client-api/references/filters.md +0 -256
  119. package/template/.agents/skills/data/prisma-client-api/references/model-queries.md +0 -281
  120. package/template/.agents/skills/data/prisma-client-api/references/query-options.md +0 -276
  121. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +0 -198
  122. package/template/.agents/skills/data/prisma-client-api/references/relations.md +0 -308
  123. package/template/.agents/skills/data/prisma-client-api/references/transactions.md +0 -184
  124. package/template/.agents/skills/design/impeccable/SKILL.md +0 -84
  125. package/template/.agents/skills/design/impeccable/reference/adapt.md +0 -312
  126. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +0 -58
  127. package/template/.agents/skills/design/impeccable/reference/android.md +0 -46
  128. package/template/.agents/skills/design/impeccable/reference/animate.md +0 -89
  129. package/template/.agents/skills/design/impeccable/reference/audit.md +0 -136
  130. package/template/.agents/skills/design/impeccable/reference/audit.native.md +0 -139
  131. package/template/.agents/skills/design/impeccable/reference/bolder.md +0 -33
  132. package/template/.agents/skills/design/impeccable/reference/clarify.md +0 -94
  133. package/template/.agents/skills/design/impeccable/reference/colorize.md +0 -86
  134. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +0 -44
  135. package/template/.agents/skills/design/impeccable/reference/craft.md +0 -5
  136. package/template/.agents/skills/design/impeccable/reference/critique.md +0 -806
  137. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +0 -37
  138. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +0 -24
  139. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +0 -38
  140. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +0 -92
  141. package/template/.agents/skills/design/impeccable/reference/delight.md +0 -70
  142. package/template/.agents/skills/design/impeccable/reference/distill.md +0 -111
  143. package/template/.agents/skills/design/impeccable/reference/doctor.md +0 -54
  144. package/template/.agents/skills/design/impeccable/reference/document.md +0 -416
  145. package/template/.agents/skills/design/impeccable/reference/extract.md +0 -69
  146. package/template/.agents/skills/design/impeccable/reference/harden.md +0 -336
  147. package/template/.agents/skills/design/impeccable/reference/hooks.md +0 -111
  148. package/template/.agents/skills/design/impeccable/reference/init.md +0 -131
  149. package/template/.agents/skills/design/impeccable/reference/ios.md +0 -51
  150. package/template/.agents/skills/design/impeccable/reference/layout.md +0 -84
  151. package/template/.agents/skills/design/impeccable/reference/live-setup.md +0 -102
  152. package/template/.agents/skills/design/impeccable/reference/live.md +0 -323
  153. package/template/.agents/skills/design/impeccable/reference/new-work.md +0 -145
  154. package/template/.agents/skills/design/impeccable/reference/onboard.md +0 -234
  155. package/template/.agents/skills/design/impeccable/reference/operate.md +0 -61
  156. package/template/.agents/skills/design/impeccable/reference/optimize.md +0 -258
  157. package/template/.agents/skills/design/impeccable/reference/overdrive.md +0 -127
  158. package/template/.agents/skills/design/impeccable/reference/polish.md +0 -105
  159. package/template/.agents/skills/design/impeccable/reference/quieter.md +0 -99
  160. package/template/.agents/skills/design/impeccable/reference/routing.md +0 -18
  161. package/template/.agents/skills/design/impeccable/reference/shape.md +0 -59
  162. package/template/.agents/skills/design/impeccable/reference/typeset.md +0 -80
  163. package/template/.agents/skills/design/impeccable/reference/visualize.md +0 -46
  164. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +0 -1022
  165. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +0 -94
  166. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +0 -391
  167. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +0 -513
  168. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +0 -814
  169. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +0 -325
  170. package/template/.agents/skills/design/impeccable/scripts/context.mjs +0 -1565
  171. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +0 -473
  172. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +0 -121
  173. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +0 -1
  174. package/template/.agents/skills/design/impeccable/scripts/detect-csp.mjs +0 -198
  175. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +0 -30
  176. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +0 -2204
  177. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +0 -432
  178. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +0 -1311
  179. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +0 -9043
  180. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +0 -51
  181. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +0 -434
  182. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +0 -1319
  183. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +0 -1241
  184. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +0 -288
  185. package/template/.agents/skills/design/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -189
  186. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +0 -18
  187. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +0 -213
  188. package/template/.agents/skills/design/impeccable/scripts/detector/profile/profiler.mjs +0 -166
  189. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +0 -635
  190. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +0 -5681
  191. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +0 -596
  192. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +0 -127
  193. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +0 -30
  194. package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +0 -148
  195. package/template/.agents/skills/design/impeccable/scripts/detector/shared/page.mjs +0 -7
  196. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +0 -329
  197. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +0 -166
  198. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +0 -457
  199. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +0 -447
  200. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +0 -819
  201. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +0 -538
  202. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +0 -2490
  203. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +0 -79
  204. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +0 -93
  205. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +0 -200
  206. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +0 -396
  207. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +0 -880
  208. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +0 -564
  209. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +0 -130
  210. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +0 -246
  211. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +0 -306
  212. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +0 -640
  213. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +0 -137
  214. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +0 -72
  215. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +0 -37
  216. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +0 -26
  217. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +0 -281
  218. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +0 -5
  219. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +0 -194
  220. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +0 -369
  221. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +0 -485
  222. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +0 -169
  223. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +0 -533
  224. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +0 -149
  225. package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +0 -42
  226. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +0 -33
  227. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +0 -146
  228. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +0 -617
  229. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +0 -60
  230. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +0 -84
  231. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +0 -28
  232. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +0 -199
  233. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +0 -47
  234. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +0 -73
  235. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +0 -143
  236. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +0 -197
  237. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +0 -49
  238. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +0 -161
  239. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +0 -17
  240. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +0 -26
  241. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +0 -71
  242. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +0 -247
  243. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +0 -70
  244. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +0 -42
  245. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +0 -149
  246. package/template/.agents/skills/design/impeccable/scripts/live/insert-ui.mjs +0 -458
  247. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +0 -142
  248. package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +0 -939
  249. package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +0 -357
  250. package/template/.agents/skills/design/impeccable/scripts/live/manual-edits-buffer.mjs +0 -152
  251. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +0 -14
  252. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +0 -139
  253. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +0 -508
  254. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +0 -563
  255. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +0 -105
  256. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +0 -105
  257. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +0 -969
  258. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +0 -1366
  259. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +0 -304
  260. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +0 -259
  261. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +0 -75
  262. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +0 -171
  263. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +0 -938
  264. package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +0 -146
  265. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +0 -242
  266. package/template/.agents/skills/design/impeccable/scripts/live-browser-session.js +0 -123
  267. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +0 -12551
  268. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +0 -1200
  269. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +0 -107
  270. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +0 -800
  271. package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +0 -51
  272. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +0 -463
  273. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +0 -292
  274. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +0 -368
  275. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +0 -430
  276. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +0 -123
  277. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +0 -1698
  278. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +0 -71
  279. package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +0 -30
  280. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +0 -927
  281. package/template/.agents/skills/design/impeccable/scripts/live.mjs +0 -334
  282. package/template/.agents/skills/design/impeccable/scripts/modern-screenshot.umd.js +0 -14
  283. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +0 -628
  284. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +0 -224
  285. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +0 -1783
  286. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +0 -74
  287. package/template/.agents/skills/design/shadcn/SKILL.md +0 -277
  288. package/template/.agents/skills/design/shadcn/agents/openai.yml +0 -5
  289. package/template/.agents/skills/design/shadcn/assets/shadcn-small.png +0 -0
  290. package/template/.agents/skills/design/shadcn/assets/shadcn.png +0 -0
  291. package/template/.agents/skills/design/shadcn/cli.md +0 -290
  292. package/template/.agents/skills/design/shadcn/customization.md +0 -209
  293. package/template/.agents/skills/design/shadcn/evals/evals.json +0 -77
  294. package/template/.agents/skills/design/shadcn/mcp.md +0 -105
  295. package/template/.agents/skills/design/shadcn/registry.md +0 -277
  296. package/template/.agents/skills/design/shadcn/rules/base-vs-radix.md +0 -306
  297. package/template/.agents/skills/design/shadcn/rules/chat.md +0 -250
  298. package/template/.agents/skills/design/shadcn/rules/composition.md +0 -213
  299. package/template/.agents/skills/design/shadcn/rules/forms.md +0 -192
  300. package/template/.agents/skills/design/shadcn/rules/icons.md +0 -101
  301. package/template/.agents/skills/design/shadcn/rules/styling.md +0 -185
  302. package/template/.agents/skills/find-skills/SKILL.md +0 -141
  303. package/template/.agents/skills/hono/SKILL.md +0 -593
  304. package/template/.agents/skills/integrations/langfuse/SKILL.md +0 -150
  305. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +0 -41
  306. package/template/.agents/skills/integrations/langfuse/references/cli.md +0 -60
  307. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +0 -35
  308. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +0 -87
  309. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +0 -121
  310. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +0 -293
  311. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +0 -35
  312. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +0 -79
  313. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -34
  314. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +0 -65
  315. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +0 -35
  316. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +0 -76
  317. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +0 -66
  318. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +0 -75
  319. package/template/.agents/skills/integrations/posthog/SKILL.md +0 -102
  320. package/template/.agents/skills/integrations/posthog/references/error-tracking-alerts.md +0 -63
  321. package/template/.agents/skills/integrations/posthog/references/error-tracking-assigning-issues.md +0 -77
  322. package/template/.agents/skills/integrations/posthog/references/error-tracking-fingerprints.md +0 -57
  323. package/template/.agents/skills/integrations/posthog/references/error-tracking-monitoring.md +0 -140
  324. package/template/.agents/skills/integrations/posthog/references/error-tracking-nextjs.md +0 -490
  325. package/template/.agents/skills/integrations/posthog/references/error-tracking-source-maps.md +0 -45
  326. package/template/.agents/skills/integrations/posthog/references/feature-flags-best-practices.md +0 -139
  327. package/template/.agents/skills/integrations/posthog/references/feature-flags-react.md +0 -302
  328. package/template/.agents/skills/integrations/posthog/references/identify-users.md +0 -202
  329. package/template/.agents/skills/integrations/posthog/references/integration-example.md +0 -706
  330. package/template/.agents/skills/integrations/posthog/references/integration-nextjs.md +0 -385
  331. package/template/.agents/skills/integrations/posthog/references/integration-step-1-begin.md +0 -43
  332. package/template/.agents/skills/integrations/posthog/references/integration-step-2-edit.md +0 -37
  333. package/template/.agents/skills/integrations/posthog/references/integration-step-3-revise.md +0 -22
  334. package/template/.agents/skills/integrations/posthog/references/integration-step-4-conclude.md +0 -38
  335. package/template/.agents/skills/integrations/posthog/references/llm-analytics-anthropic.md +0 -200
  336. package/template/.agents/skills/integrations/posthog/references/llm-analytics-basics.md +0 -62
  337. package/template/.agents/skills/integrations/posthog/references/llm-analytics-costs.md +0 -197
  338. package/template/.agents/skills/integrations/posthog/references/llm-analytics-manual-capture.md +0 -397
  339. package/template/.agents/skills/integrations/posthog/references/llm-analytics-traces.md +0 -98
  340. package/template/.agents/skills/integrations/posthog/references/llm-analytics-vercel-ai.md +0 -120
  341. package/template/.agents/skills/repo/repo-ci/SKILL.md +0 -265
  342. package/template/.agents/skills/repo/repo-init-next-js/SKILL.md +0 -129
  343. package/template/.agents/skills/repo/repo-init-next-js/references/file-contents.md +0 -800
  344. package/template/.agents/skills/repo/repo-init-next-js/scripts/setup.sh +0 -47
  345. package/template/.agents/skills/repo/repo-init-node/SKILL.md +0 -196
  346. package/template/.agents/skills/skill-creator/LICENSE.txt +0 -202
  347. package/template/.agents/skills/skill-creator/SKILL.md +0 -485
  348. package/template/.agents/skills/skill-creator/agents/analyzer.md +0 -274
  349. package/template/.agents/skills/skill-creator/agents/comparator.md +0 -202
  350. package/template/.agents/skills/skill-creator/agents/grader.md +0 -223
  351. package/template/.agents/skills/skill-creator/assets/eval_review.html +0 -146
  352. package/template/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  353. package/template/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  354. package/template/.agents/skills/skill-creator/references/schemas.md +0 -430
  355. package/template/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  356. package/template/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  357. package/template/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
  358. package/template/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
  359. package/template/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
  360. package/template/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
  361. package/template/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
  362. package/template/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
  363. package/template/.agents/skills/skill-creator/scripts/utils.py +0 -47
  364. package/template/.agents/skills/supabase/CHANGELOG.md +0 -78
  365. package/template/.agents/skills/supabase/SKILL.md +0 -149
  366. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +0 -17
  367. package/template/.agents/skills/supabase/references/skill-feedback.md +0 -17
  368. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +0 -73
  369. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
  370. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +0 -170
  371. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +0 -39
  372. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +0 -34
  373. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
  374. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
  375. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
  376. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
  377. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
  378. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
  379. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
  380. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
  381. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
  382. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
  383. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
  384. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
  385. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
  386. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
  387. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
  388. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
  389. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
  390. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
  391. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
  392. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
  393. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
  394. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
  395. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
  396. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
  397. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
  398. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
  399. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
  400. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
  401. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
  402. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
  403. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -63
  404. package/template/.agents/skills/writing/humanizer/LICENSE +0 -21
  405. package/template/.agents/skills/writing/humanizer/README.md +0 -209
  406. package/template/.agents/skills/writing/humanizer/SKILL.md +0 -456
  407. package/template/.agents/upstreams.json +0 -115
@@ -1,1311 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { finding } from './findings.mjs';
6
- import { GENERIC_FONTS } from './shared/constants.mjs';
7
- import { parseAnyColor, resolveLengthPx } from './rules/checks.mjs';
8
-
9
- const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
10
- const FALLBACK_DIRS = ['.agents/context', 'docs'];
11
- // Files/dirs whose presence marks a directory as a project root. Mirrors the
12
- // walk-up semantics of skill/scripts/context.mjs (`resolveProject`), which the
13
- // CLI can't import (separate tree). `.git` and `package.json` are the common
14
- // boundaries; `.impeccable` is our own project marker.
15
- const PROJECT_ROOT_MARKERS = ['.git', 'package.json', '.impeccable'];
16
- // Monorepo-root recognition, mirroring context.mjs's isMonorepoRoot: declared
17
- // workspace globs (package.json `workspaces`, pnpm-workspace.yaml `packages:`)
18
- // or a marker file beside apps/ or packages/ children.
19
- const MONOREPO_MARKER_FILES = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
20
- const MONOREPO_FALLBACK_PROJECT_DIRS = ['apps', 'packages'];
21
- const COLOR_CHANNEL_TOLERANCE = 6;
22
- // Shadow blacks at different alphas are different tokens (0.28 vs 0.55 is the
23
- // difference between a documented shadow and drift), so shadow matching cannot
24
- // reuse the r/g/b-only channel tolerance.
25
- const SHADOW_ALPHA_TOLERANCE = 0.02;
26
- const RADIUS_TOLERANCE_PX = 0.5;
27
- const FONT_SIZE_TOLERANCE_PX = 0.5;
28
- const FONT_SIZE_LITERAL_RE = /^-?[\d.]+(?:px|rem)$/;
29
-
30
- const CSS_COLOR_RE = /#[0-9a-f]{3,8}\b|rgba?\([^)]+\)|oklch\([^)]+\)|hsla?\([^)]+\)/gi;
31
- const FONT_DECL_RE = /font-family\s*:\s*([^;}\n]+)/gi;
32
- const FONT_JS_RE = /fontFamily\s*[:=]\s*["'`]([^"'`]+)["'`]/g;
33
- const GOOGLE_FONT_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi;
34
- const BORDER_RADIUS_RE = /border-radius\s*:\s*([^;}\n]+)/gi;
35
- const BORDER_RADIUS_JS_RE = /borderRadius\s*[:=]\s*["'`]([^"'`]+)["'`]/g;
36
- const FONT_SIZE_DECL_RE = /font-size\s*:\s*([^;}\n]+)/gi;
37
- const FONT_SIZE_JS_RE = /fontSize\s*[:=]\s*["'`]([^"'`]+)["'`]/g;
38
- const TAILWIND_FONT_SIZE_RE = /\btext-\[(-?[\d.]+(?:px|rem))\]/g;
39
- const STATIC_DESIGN_SKIP_TAGS = new Set(['head', 'title', 'meta', 'link', 'style', 'script', 'noscript', 'template', 'source']);
40
-
41
- function firstExisting(dir, names) {
42
- for (const name of names) {
43
- const abs = path.join(dir, name);
44
- if (fs.existsSync(abs)) return abs;
45
- }
46
- return null;
47
- }
48
-
49
- function resolveDesignMdPath(cwd = process.cwd()) {
50
- const root = firstExisting(cwd, DESIGN_NAMES);
51
- if (root) return { path: root, contextDir: cwd };
52
-
53
- for (const rel of FALLBACK_DIRS) {
54
- const dir = path.resolve(cwd, rel);
55
- const found = firstExisting(dir, DESIGN_NAMES);
56
- if (found) return { path: found, contextDir: dir };
57
- }
58
-
59
- return null;
60
- }
61
-
62
- function resolveDesignSidecarPath(cwd = process.cwd(), contextDir = cwd) {
63
- const candidates = [
64
- path.join(cwd, '.impeccable', 'design.json'),
65
- path.join(cwd, 'DESIGN.json'),
66
- path.join(contextDir, 'DESIGN.json'),
67
- ];
68
- return candidates.find((candidate, index) =>
69
- candidates.indexOf(candidate) === index && fs.existsSync(candidate)
70
- ) || null;
71
- }
72
-
73
- function parseFrontmatter(md) {
74
- const lines = String(md || '').split(/\r?\n/);
75
- if (lines[0]?.trim() !== '---') return null;
76
- let end = -1;
77
- for (let i = 1; i < lines.length; i++) {
78
- if (lines[i].trim() === '---') { end = i; break; }
79
- }
80
- if (end === -1) return null;
81
- try {
82
- return parseYamlSubset(lines.slice(1, end).join('\n'));
83
- } catch {
84
- return null;
85
- }
86
- }
87
-
88
- function parseYamlSubset(yaml) {
89
- const root = {};
90
- const stack = [{ indent: -1, obj: root }];
91
-
92
- for (const raw of String(yaml || '').split(/\r?\n/)) {
93
- if (!raw.trim() || /^\s*#/.test(raw)) continue;
94
- const indent = raw.match(/^\s*/)[0].length;
95
- const content = raw.slice(indent);
96
- const colonIdx = findTopLevelColon(content);
97
- if (colonIdx === -1) continue;
98
-
99
- while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
100
-
101
- const key = unquoteYamlKey(content.slice(0, colonIdx).trim());
102
- const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim());
103
- const parent = stack[stack.length - 1].obj;
104
-
105
- if (rest === '') {
106
- const obj = {};
107
- parent[key] = obj;
108
- stack.push({ indent, obj });
109
- } else {
110
- parent[key] = parseScalar(rest);
111
- }
112
- }
113
-
114
- return root;
115
- }
116
-
117
- function findTopLevelColon(s) {
118
- let inQuote = null;
119
- for (let i = 0; i < s.length; i++) {
120
- const ch = s[i];
121
- if (inQuote) {
122
- if (ch === inQuote && s[i - 1] !== '\\') inQuote = null;
123
- } else if (ch === '"' || ch === "'") {
124
- inQuote = ch;
125
- } else if (ch === ':') {
126
- return i;
127
- }
128
- }
129
- return -1;
130
- }
131
-
132
- function unquoteYamlKey(key) {
133
- if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
134
- return key.slice(1, -1);
135
- }
136
- return key;
137
- }
138
-
139
- function stripInlineYamlComment(s) {
140
- let inQuote = null;
141
- for (let i = 0; i < s.length; i++) {
142
- const ch = s[i];
143
- if (inQuote) {
144
- if (ch === inQuote && s[i - 1] !== '\\') inQuote = null;
145
- } else if (ch === '"' || ch === "'") {
146
- inQuote = ch;
147
- } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) {
148
- return s.slice(0, i).trimEnd();
149
- }
150
- }
151
- return s;
152
- }
153
-
154
- // YAML double-quoted scalars process backslash escapes. Stripping the outer
155
- // quotes without unescaping leaves them in place, so a nested font family like
156
- // fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
157
- // reaches allowedFonts as '\"ibm plex sans' and never matches the same family
158
- // declared in CSS. Scanner instead of a regex: the escape set is small and the
159
- // backslash handling stays readable.
160
- // The full YAML 1.2 double-quote escape set (spec section 5.7).
161
- const YAML_SIMPLE_ESCAPES = {
162
- '0': '\0',
163
- a: '\x07',
164
- b: '\b',
165
- t: '\t',
166
- n: '\n',
167
- v: '\v',
168
- f: '\f',
169
- r: '\r',
170
- e: '\x1b',
171
- ' ': ' ',
172
- '"': '"',
173
- '/': '/',
174
- '\\': '\\',
175
- N: '\u0085',
176
- _: '\u00a0',
177
- L: '\u2028',
178
- P: '\u2029',
179
- };
180
- const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 };
181
-
182
- function unescapeYamlDoubleQuoted(body) {
183
- let out = '';
184
- for (let i = 0; i < body.length; i++) {
185
- const ch = body[i];
186
- if (ch !== '\\' || i === body.length - 1) {
187
- out += ch;
188
- continue;
189
- }
190
- const next = body[i + 1];
191
- if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) {
192
- out += YAML_SIMPLE_ESCAPES[next];
193
- i++;
194
- continue;
195
- }
196
- // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay
197
- // literal rather than corrupting the rest of the scalar.
198
- const hexLen = YAML_HEX_ESCAPE_LENGTHS[next];
199
- if (hexLen) {
200
- const hex = body.slice(i + 2, i + 2 + hexLen);
201
- const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1;
202
- if (codePoint >= 0 && codePoint <= 0x10ffff) {
203
- out += String.fromCodePoint(codePoint);
204
- i += 1 + hexLen;
205
- continue;
206
- }
207
- }
208
- out += ch;
209
- }
210
- return out;
211
- }
212
-
213
- function parseScalar(raw) {
214
- const s = raw.trim();
215
- if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
216
- return unescapeYamlDoubleQuoted(s.slice(1, -1));
217
- }
218
- // Single-quoted YAML escapes only the quote itself, by doubling it.
219
- if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
220
- return s.slice(1, -1).split("''").join("'");
221
- }
222
- if (s === 'true') return true;
223
- if (s === 'false') return false;
224
- if (s === 'null' || s === '~') return null;
225
- if (/^-?\d+$/.test(s)) return Number(s);
226
- if (/^-?\d*\.\d+$/.test(s)) return Number(s);
227
- return s;
228
- }
229
-
230
- function safeReadJson(filePath) {
231
- if (!filePath) return null;
232
- try {
233
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
234
- } catch {
235
- return null;
236
- }
237
- }
238
-
239
- function normalizeFontName(value) {
240
- return String(value || '')
241
- .trim()
242
- .replace(/\s*!important\s*$/i, '')
243
- .trim()
244
- .replace(/^["']|["']$/g, '')
245
- .replace(/\+/g, ' ')
246
- .replace(/\s+/g, ' ')
247
- .toLowerCase();
248
- }
249
-
250
- function splitFontStack(stack) {
251
- return String(stack || '')
252
- .replace(/\s*!important\s*$/i, '')
253
- .split(',')
254
- .map(normalizeFontName)
255
- .filter(Boolean);
256
- }
257
-
258
- function primaryFont(stack) {
259
- if (!stack || /var\(/i.test(stack) || !isLiteralFontStack(stack)) return '';
260
- return splitFontStack(stack).find(font => !GENERIC_FONTS.has(font)) || '';
261
- }
262
-
263
- function isLiteralFontStack(stack) {
264
- const text = String(stack || '');
265
- return !/[$`{}]|\s\+\s|\|\|/.test(text);
266
- }
267
-
268
- function cssColorLabel(raw) {
269
- return String(raw || '').trim().replace(/\s+/g, ' ');
270
- }
271
-
272
- function colorKey(color) {
273
- if (!color) return '';
274
- return `${color.r},${color.g},${color.b}`;
275
- }
276
-
277
- function colorsClose(a, b) {
278
- if (!a || !b) return false;
279
- return Math.max(
280
- Math.abs(a.r - b.r),
281
- Math.abs(a.g - b.g),
282
- Math.abs(a.b - b.b),
283
- ) <= COLOR_CHANNEL_TOLERANCE;
284
- }
285
-
286
- function hslToRgb(H, S, L, alpha = 1) {
287
- const h = (((H % 360) + 360) % 360) / 360;
288
- const s = Math.max(0, Math.min(1, S));
289
- const l = Math.max(0, Math.min(1, L));
290
- const hue2rgb = (p, q, t) => {
291
- if (t < 0) t += 1;
292
- if (t > 1) t -= 1;
293
- if (t < 1 / 6) return p + (q - p) * 6 * t;
294
- if (t < 1 / 2) return q;
295
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
296
- return p;
297
- };
298
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
299
- const p = 2 * l - q;
300
- return {
301
- r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
302
- g: Math.round(hue2rgb(p, q, h) * 255),
303
- b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
304
- a: alpha,
305
- };
306
- }
307
-
308
- function parseDesignColor(value) {
309
- const text = String(value || '').trim();
310
- const parsed = parseAnyColor(text);
311
- if (parsed) return parsed;
312
- const hsl = text.match(/hsla?\(\s*([-\d.]+)(?:deg)?\s*,?\s*([\d.]+)%\s*,?\s*([\d.]+)%(?:\s*[,/]\s*([\d.]+))?\s*\)/i);
313
- if (hsl) {
314
- return hslToRgb(
315
- parseFloat(hsl[1]),
316
- parseFloat(hsl[2]) / 100,
317
- parseFloat(hsl[3]) / 100,
318
- hsl[4] !== undefined ? parseFloat(hsl[4]) : 1,
319
- );
320
- }
321
- return null;
322
- }
323
-
324
- function addDesignColor(out, value, label) {
325
- const parsed = parseDesignColor(value);
326
- if (!parsed) return;
327
- const key = colorKey(parsed);
328
- if (!out.allowedColorKeys.has(key)) {
329
- out.allowedColorKeys.set(key, { color: parsed, labels: [] });
330
- }
331
- out.allowedColorKeys.get(key).labels.push(label || cssColorLabel(value));
332
- }
333
-
334
- function addColorObject(out, colors, prefix = 'colors') {
335
- if (!colors || typeof colors !== 'object') return;
336
- for (const [name, value] of Object.entries(colors)) {
337
- if (typeof value === 'string') {
338
- addDesignColor(out, value, `${prefix}.${name}`);
339
- }
340
- }
341
- }
342
-
343
- function addSidecarColors(out, sidecar) {
344
- const colorMeta = sidecar?.extensions?.colorMeta;
345
- if (!colorMeta || typeof colorMeta !== 'object') return;
346
-
347
- for (const [name, meta] of Object.entries(colorMeta)) {
348
- if (!meta || typeof meta !== 'object') continue;
349
- if (typeof meta.canonical === 'string') addDesignColor(out, meta.canonical, `sidecar.${name}`);
350
- if (Array.isArray(meta.tonalRamp)) {
351
- for (const [index, value] of meta.tonalRamp.entries()) {
352
- if (typeof value === 'string') addDesignColor(out, value, `sidecar.${name}.tonalRamp[${index}]`);
353
- }
354
- }
355
- }
356
- }
357
-
358
- function addTypographyFonts(out, typography) {
359
- if (!typography || typeof typography !== 'object') return;
360
- for (const role of Object.values(typography)) {
361
- if (!role || typeof role !== 'object') continue;
362
- if (typeof role.fontFamily !== 'string') continue;
363
- for (const font of splitFontStack(role.fontFamily)) {
364
- if (!GENERIC_FONTS.has(font)) out.allowedFonts.add(font);
365
- }
366
- }
367
- }
368
-
369
- function addFontSizeStep(out, raw, { fluid = false } = {}) {
370
- const text = String(raw ?? '').trim().toLowerCase();
371
- if (!FONT_SIZE_LITERAL_RE.test(text)) return;
372
- const px = resolveLengthPx(text, 16);
373
- if (px == null || !Number.isFinite(px) || px <= 0) return;
374
- out.allowedFontSizes.push({ value: text, px, fluid });
375
- }
376
-
377
- // Split a fluid value into its three terms, or null when it is not a
378
- // well-formed clamp(). Used both to read DESIGN.md's fluid roles and to
379
- // validate fluid values in source, so the two stay symmetric.
380
- function parseClampArgs(raw) {
381
- const match = /^clamp\(\s*([\s\S]+)\s*\)$/i.exec(String(raw ?? '').trim());
382
- if (!match) return null;
383
- const args = splitTopLevelArgs(match[1]);
384
- return args.length === 3 ? args : null;
385
- }
386
-
387
- // A fluid role declares its two fixed endpoints and interpolates between them
388
- // with a viewport unit. Both endpoints are documented sizes, so they belong in
389
- // the allowlist; the middle term is viewport-relative and never a fixed step.
390
- // Endpoints are marked `fluid` because they do not *enumerate* a ramp: see
391
- // `hasFontSizes` below for why that distinction has to survive.
392
- function addClampEndpoints(out, raw) {
393
- const args = parseClampArgs(raw);
394
- if (!args) return false;
395
- addFontSizeStep(out, args[0], { fluid: true });
396
- addFontSizeStep(out, args[2], { fluid: true });
397
- return true;
398
- }
399
-
400
- function splitTopLevelArgs(s) {
401
- const args = [];
402
- let depth = 0;
403
- let current = '';
404
- for (const ch of String(s)) {
405
- if (ch === '(') depth++;
406
- else if (ch === ')') depth--;
407
- if (ch === ',' && depth === 0) {
408
- args.push(current.trim());
409
- current = '';
410
- continue;
411
- }
412
- current += ch;
413
- }
414
- if (current.trim()) args.push(current.trim());
415
- return args;
416
- }
417
-
418
- function addTypographySizes(out, typography) {
419
- if (!typography || typeof typography !== 'object') return;
420
-
421
- // `scale` is the enumerated ramp: a name -> size map, since the frontmatter
422
- // parser has no list support. It sits alongside the named roles.
423
- const scale = typography.scale;
424
- if (scale && typeof scale === 'object') {
425
- for (const value of Object.values(scale)) {
426
- if (typeof value !== 'string' && typeof value !== 'number') continue;
427
- addFontSizeStep(out, value);
428
- }
429
- }
430
-
431
- for (const [name, role] of Object.entries(typography)) {
432
- if (name === 'scale') continue;
433
- if (!role || typeof role !== 'object') continue;
434
- const raw = String(role.fontSize ?? '').trim().toLowerCase();
435
- if (addClampEndpoints(out, raw)) continue;
436
- addFontSizeStep(out, raw);
437
- }
438
- }
439
-
440
- function addRoundedScale(out, rounded) {
441
- if (!rounded || typeof rounded !== 'object') return;
442
- for (const [rawName, value] of Object.entries(rounded)) {
443
- const name = unquoteYamlKey(rawName).toLowerCase();
444
- addRoundedToken(out, name, value);
445
- }
446
- }
447
-
448
- function addRoundedToken(out, name, value) {
449
- if (typeof value !== 'string' && typeof value !== 'number') return;
450
- const raw = String(value).trim();
451
- if (!raw || /var\(/i.test(raw) || raw.includes('%')) return;
452
- const px = resolveLengthPx(raw, 16);
453
- if (px == null || !Number.isFinite(px)) return;
454
- out.allowedRadii.push({ name, value: raw, px });
455
- if (/(^|\.)(full|pill|round|rounded-full)$/.test(name)) out.hasPillRadius = true;
456
- }
457
-
458
- function addSidecarRadii(out, sidecar) {
459
- const roundedMeta = sidecar?.extensions?.roundedMeta;
460
- if (!roundedMeta || typeof roundedMeta !== 'object') return;
461
-
462
- for (const [rawName, meta] of Object.entries(roundedMeta)) {
463
- const name = unquoteYamlKey(rawName).toLowerCase();
464
- if (typeof meta === 'string' || typeof meta === 'number') {
465
- addRoundedToken(out, `sidecar.${name}`, meta);
466
- continue;
467
- }
468
- if (!meta || typeof meta !== 'object') continue;
469
- for (const key of ['canonical', 'value']) {
470
- if (typeof meta[key] === 'string' || typeof meta[key] === 'number') {
471
- addRoundedToken(out, `sidecar.${name}.${key}`, meta[key]);
472
- }
473
- }
474
- for (const key of ['values', 'aliases']) {
475
- if (!Array.isArray(meta[key])) continue;
476
- for (const [index, value] of meta[key].entries()) {
477
- addRoundedToken(out, `sidecar.${name}.${key}[${index}]`, value);
478
- }
479
- }
480
- if (/^(full|pill|round|rounded-full)$/.test(name) || /^(full|pill|round)$/i.test(String(meta.role || ''))) {
481
- out.hasPillRadius = true;
482
- }
483
- }
484
- }
485
-
486
- // Sidecar `extensions.shadows` entries ({ name, value, purpose }) carry the
487
- // documented shadow vocabulary that Stitch's frontmatter schema can't hold.
488
- // Their colors go into a separate allowlist — NOT allowedColorKeys — because a
489
- // shadow black is only documented *as a shadow*: feeding it into the general
490
- // color allowlist would legalize #000 as a page ground (alpha is dropped from
491
- // colorKey), which is the hole issue #547 warns against.
492
- function addSidecarShadows(out, sidecar) {
493
- const shadows = sidecar?.extensions?.shadows;
494
- if (!Array.isArray(shadows)) return;
495
-
496
- for (const entry of shadows) {
497
- if (typeof entry?.value !== 'string') continue;
498
- for (const match of entry.value.matchAll(CSS_COLOR_RE)) {
499
- const parsed = parseDesignColor(match[0]);
500
- if (parsed) out.allowedShadowColors.push({ color: parsed });
501
- }
502
- }
503
- }
504
-
505
- function normalizeDesignSystem(input = {}) {
506
- const frontmatter = input.frontmatter || {};
507
- const sidecar = input.sidecar || null;
508
- const out = {
509
- present: true,
510
- sourcePath: input.sourcePath || null,
511
- sidecarPath: input.sidecarPath || null,
512
- mdNewerThanJson: input.mdNewerThanJson === true,
513
- allowedFonts: new Set(),
514
- allowedColorKeys: new Map(),
515
- allowedRadii: [],
516
- allowedFontSizes: [],
517
- allowedShadowColors: [],
518
- hasPillRadius: false,
519
- };
520
-
521
- addTypographyFonts(out, frontmatter.typography);
522
- addTypographySizes(out, frontmatter.typography);
523
- addColorObject(out, frontmatter.colors);
524
- addSidecarColors(out, sidecar);
525
- addRoundedScale(out, frontmatter.rounded);
526
- addSidecarRadii(out, sidecar);
527
- addSidecarShadows(out, sidecar);
528
-
529
- out.hasFonts = out.allowedFonts.size > 0;
530
- out.hasColors = out.allowedColorKeys.size > 0;
531
- out.hasRadii = out.allowedRadii.length > 0;
532
- // Gate on *enumerated* steps only. A fully fluid system declares clamp
533
- // endpoints but no discrete ramp, so treating those endpoints as the whole
534
- // allowlist would flag every intermediate size. Abstain instead.
535
- out.hasFontSizes = out.allowedFontSizes.some(entry => !entry.fluid);
536
- return out;
537
- }
538
-
539
- function loadDesignSystemForCwd(cwd = process.cwd()) {
540
- const md = resolveDesignMdPath(cwd);
541
- if (!md) return null;
542
-
543
- let frontmatter = null;
544
- let mdStat = null;
545
- try {
546
- mdStat = fs.statSync(md.path);
547
- frontmatter = parseFrontmatter(fs.readFileSync(md.path, 'utf-8'));
548
- } catch {
549
- return null;
550
- }
551
- if (!frontmatter || typeof frontmatter !== 'object') return null;
552
-
553
- const sidecarPath = resolveDesignSidecarPath(cwd, md.contextDir);
554
- const sidecar = safeReadJson(sidecarPath);
555
- let sidecarStat = null;
556
- try {
557
- if (sidecarPath) sidecarStat = fs.statSync(sidecarPath);
558
- } catch {
559
- sidecarStat = null;
560
- }
561
-
562
- return normalizeDesignSystem({
563
- frontmatter,
564
- sidecar,
565
- sourcePath: md.path,
566
- sidecarPath,
567
- mdNewerThanJson: !!(mdStat && sidecarStat && mdStat.mtimeMs > sidecarStat.mtimeMs + 1000),
568
- });
569
- }
570
-
571
- // Directory to begin the project-root walk from, given a scan target that may
572
- // be a file or a directory (and may not exist yet).
573
- function designSystemStartDir(targetPath, cwd = process.cwd()) {
574
- const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
575
- try {
576
- return fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
577
- } catch {
578
- // Nonexistent path: treat an extension-bearing leaf as a file.
579
- return path.extname(abs) ? path.dirname(abs) : abs;
580
- }
581
- }
582
-
583
- // Same two groups as context.mjs's readProjectPatternGroups: Impeccable
584
- // projectRoots govern any path they match (positive or negated); package-manager
585
- // globs only apply to paths the Impeccable group does not match.
586
- function readWorkspacePatternGroups(dir) {
587
- const impeccable = [];
588
- for (const name of ['config.json', 'config.local.json']) {
589
- const roots = safeReadJson(path.join(dir, '.impeccable', name))?.projectRoots;
590
- if (Array.isArray(roots)) {
591
- impeccable.push(...roots.filter(entry => typeof entry === 'string' && entry.trim()).map(entry => entry.trim()));
592
- }
593
- }
594
- const pkg = [];
595
- const workspaces = safeReadJson(path.join(dir, 'package.json'))?.workspaces;
596
- if (Array.isArray(workspaces)) pkg.push(...workspaces);
597
- else if (Array.isArray(workspaces?.packages)) pkg.push(...workspaces.packages);
598
- const lernaPackages = safeReadJson(path.join(dir, 'lerna.json'))?.packages;
599
- if (Array.isArray(lernaPackages)) pkg.push(...lernaPackages);
600
- try {
601
- let inPackages = false;
602
- for (const line of fs.readFileSync(path.join(dir, 'pnpm-workspace.yaml'), 'utf-8').split(/\r?\n/)) {
603
- const trimmed = stripInlineYamlComment(line).trim();
604
- if (!trimmed || trimmed.startsWith('#')) continue;
605
- const flow = trimmed.match(/^packages:\s*\[(.*)\]\s*$/);
606
- if (flow) {
607
- pkg.push(...flow[1].split(',').map(entry => entry.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean));
608
- break;
609
- }
610
- if (/^packages:\s*$/.test(trimmed)) { inPackages = true; continue; }
611
- if (!inPackages) continue;
612
- const item = trimmed.match(/^-\s*(.+)$/);
613
- if (item) pkg.push(item[1].trim().replace(/^['"]|['"]$/g, ''));
614
- else if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) break;
615
- }
616
- } catch { /* no pnpm-workspace.yaml */ }
617
- return [impeccable, pkg];
618
- }
619
-
620
- function readWorkspacePatterns(dir) {
621
- return readWorkspacePatternGroups(dir).flat();
622
- }
623
-
624
- function isMonorepoRoot(dir) {
625
- if (readWorkspacePatterns(dir).some(pattern => !String(pattern).trim().startsWith('!'))) return true;
626
- if (!MONOREPO_MARKER_FILES.some(file => fs.existsSync(path.join(dir, file)))) return false;
627
- return MONOREPO_FALLBACK_PROJECT_DIRS.some(name => {
628
- try {
629
- return fs.readdirSync(path.join(dir, name), { withFileTypes: true }).some(entry => entry.isDirectory());
630
- } catch {
631
- return false;
632
- }
633
- });
634
- }
635
-
636
- function monorepoOwnsPath(root, boundaryDir) {
637
- const rel = path.relative(root, boundaryDir);
638
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
639
- const relSegments = rel.split(path.sep).filter(Boolean);
640
-
641
- function normalizeWorkspacePattern(pattern) {
642
- return String(pattern || '')
643
- .trim()
644
- .replace(/^['"]|['"]$/g, '')
645
- .replace(/^\.\//, '')
646
- .replace(/\/+$/, '');
647
- }
648
-
649
- function escapeRegExp(s) {
650
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
651
- }
652
-
653
- function segmentMatches(patternSegment, relSegment) {
654
- if (patternSegment === '*') return true;
655
- if (!patternSegment.includes('*')) return patternSegment === relSegment;
656
- const re = new RegExp(`^${escapeRegExp(patternSegment).replace(/\\\*/g, '[^/]*')}$`);
657
- return re.test(relSegment);
658
- }
659
-
660
- function matchGlobSegments(patternSegments, relSegments) {
661
- function rec(pi, ri) {
662
- if (pi === patternSegments.length) return ri === relSegments.length;
663
- if (patternSegments[pi] === '**') {
664
- if (pi === patternSegments.length - 1) return true;
665
- for (let k = ri; k <= relSegments.length; k++) {
666
- if (rec(pi + 1, k)) return true;
667
- }
668
- return false;
669
- }
670
- if (ri >= relSegments.length) return false;
671
- if (!segmentMatches(patternSegments[pi], relSegments[ri])) return false;
672
- return rec(pi + 1, ri + 1);
673
- }
674
- return rec(0, 0);
675
- }
676
-
677
- // Negations like !packages/excluded must also cover nested dirs under that path.
678
- function matchesNegation(pattern) {
679
- const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
680
- if (!patternSegments.length) return false;
681
- if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
682
- if (relSegments.length < patternSegments.length) return false;
683
- for (let i = 0; i < patternSegments.length; i++) {
684
- if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
685
- }
686
- return true;
687
- }
688
-
689
- // Positive globs identify workspace packages at exact depth (`*` is a direct
690
- // child). A nested package.json under that package is still owned: the
691
- // ancestor directory of glob length must itself be a package.
692
- function positiveOwns(pattern) {
693
- const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
694
- if (!patternSegments.length) return false;
695
- if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
696
- if (relSegments.length < patternSegments.length) return false;
697
- for (let i = 0; i < patternSegments.length; i++) {
698
- if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
699
- }
700
- if (relSegments.length === patternSegments.length) return true;
701
- const ancestorDir = path.join(root, ...relSegments.slice(0, patternSegments.length));
702
- return fs.existsSync(path.join(ancestorDir, 'package.json'));
703
- }
704
-
705
- function groupOwns(rawPatterns) {
706
- const patterns = rawPatterns.map(normalizeWorkspacePattern).filter(Boolean);
707
- if (!patterns.length) return null;
708
- const excluded = patterns.some((pattern) => (
709
- pattern.startsWith('!') && matchesNegation(pattern.slice(1))
710
- ));
711
- const included = patterns.filter((pattern) => !pattern.startsWith('!')).some(positiveOwns);
712
- if (!excluded && !included) return null;
713
- if (excluded) return false;
714
- return true;
715
- }
716
-
717
- const [impeccable, pkg] = readWorkspacePatternGroups(root);
718
- const fromImpeccable = groupOwns(impeccable);
719
- if (fromImpeccable !== null) return fromImpeccable;
720
- const fromPkg = groupOwns(pkg);
721
- if (fromPkg !== null) return fromPkg;
722
- if ([...impeccable, ...pkg].some((pattern) => !normalizeWorkspacePattern(pattern).startsWith('!'))) {
723
- return false;
724
- }
725
- return relSegments.length >= 2 && MONOREPO_FALLBACK_PROJECT_DIRS.includes(relSegments[0]);
726
- }
727
-
728
- // Both forms of the home directory. The walk compares path strings, and a
729
- // symlinked home (e.g. /home -> /var/home) never string-matches the physical
730
- // paths a cwd-resolved target produces, which would let the post-boundary walk
731
- // sail through $HOME and inherit from it.
732
- function homeDirForms() {
733
- const homeDir = path.resolve(os.homedir());
734
- const forms = new Set([homeDir]);
735
- try {
736
- forms.add(fs.realpathSync(homeDir));
737
- } catch { /* keep the logical form only */ }
738
- return forms;
739
- }
740
-
741
- // Walk up from `startDir` to the directory that governs the target's design
742
- // system, mirroring skill/scripts/context.mjs's project-boundary semantics:
743
- //
744
- // - A directory carrying a DESIGN.md (directly or in a fallback dir) IS the
745
- // design root — that's where the rules live.
746
- // - A directory carrying a project marker (.git / package.json / .impeccable)
747
- // but no DESIGN.md is a project BOUNDARY. A nested package.json inherits
748
- // the ancestor DESIGN.md only when that ancestor's workspace declarations
749
- // include the path (negations win; a nested package under a matched
750
- // workspace still inherits). Marker-only roots (turbo/nx/lerna/pnpm
751
- // with no globs) still own apps/<name> and packages/<name>. A stray nested
752
- // package that matches no glob does not inherit. This is detect's
753
- // contamination contract, not skill-context's repoRoot fallback for
754
- // excluded paths. A nested separate repository (.git with no workspace
755
- // declaration) still inherits nothing (issue #570).
756
- // - Reaching the home directory / filesystem root with neither means no
757
- // design system at all — never process.cwd()'s.
758
- //
759
- // Returns { dir, hasDesign } for the stopping directory, or null when the walk
760
- // runs out. This is the fix for cross-project contamination.
761
- export function findDesignRoot(startDir) {
762
- let dir = path.resolve(startDir);
763
- const homeDirs = homeDirForms();
764
- let boundary = null;
765
- while (true) {
766
- if (!boundary && resolveDesignMdPath(dir)) return { dir, hasDesign: true };
767
- if (boundary) {
768
- // Past the boundary the walk only looks for the monorepo root that owns
769
- // the workspace path (workspace globs including negations, or marker-only
770
- // apps/packages fallback). Monorepo-root before .git, same order as
771
- // context.mjs: a workspace root carrying its own .git is still recognized,
772
- // while a .git that declares no workspaces is a separate repository and
773
- // stops the walk with nothing inherited. The home directory is never an
774
- // owning root, same as context.mjs's findMonorepoRoot, which stops at
775
- // homeDir before its monorepo check.
776
- if (!homeDirs.has(dir) && isMonorepoRoot(dir)) {
777
- if (monorepoOwnsPath(dir, boundary.dir)) return { dir, hasDesign: !!resolveDesignMdPath(dir) };
778
- return boundary;
779
- }
780
- if (fs.existsSync(path.join(dir, '.git'))) return boundary;
781
- } else if (PROJECT_ROOT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) {
782
- boundary = { dir, hasDesign: false };
783
- // A boundary that is itself a monorepo root, or a separate repository
784
- // with its own .git, inherits nothing from above.
785
- if (isMonorepoRoot(dir) || fs.existsSync(path.join(dir, '.git'))) return boundary;
786
- }
787
- if (homeDirs.has(dir)) return boundary;
788
- const parent = path.dirname(dir);
789
- if (parent === dir) return boundary;
790
- dir = parent;
791
- }
792
- }
793
-
794
- // Resolve the design system that governs a specific scan target, by walking up
795
- // from the target's own location — never process.cwd(). Scanning project B's
796
- // files from inside project A applies B's DESIGN.md (or none), not A's.
797
- //
798
- // Pass a `cache` Map to memoize by resolved design root across a multi-file
799
- // scan; a target with no design root above it resolves to null.
800
- export function loadDesignSystemForTarget(targetPath, { cache, cwd = process.cwd() } = {}) {
801
- const startDir = designSystemStartDir(targetPath, cwd);
802
- const found = findDesignRoot(startDir);
803
- const key = found ? `root:${found.dir}` : '\0none';
804
- if (cache && cache.has(key)) return cache.get(key);
805
- const loaded = found?.hasDesign ? loadDesignSystemForCwd(found.dir) : null;
806
- if (cache) cache.set(key, loaded);
807
- return loaded;
808
- }
809
-
810
- function isAllowedFont(font, designSystem) {
811
- if (!font || GENERIC_FONTS.has(font)) return true;
812
- if (!designSystem?.hasFonts) return true;
813
- return designSystem.allowedFonts.has(font);
814
- }
815
-
816
- function isAllowedColorRaw(raw, designSystem) {
817
- if (!designSystem?.hasColors) return true;
818
- const text = String(raw || '').trim().toLowerCase();
819
- if (!text || text === 'transparent' || text === 'currentcolor' || text === 'inherit' || text === 'initial') return true;
820
- if (text.includes('var(')) return true;
821
- const parsed = parseDesignColor(text);
822
- if (!parsed) return true;
823
- if ((parsed.a ?? 1) <= 0.05) return true;
824
- for (const entry of designSystem.allowedColorKeys.values()) {
825
- if (colorsClose(parsed, entry.color)) return true;
826
- }
827
- return false;
828
- }
829
-
830
- // A color is a documented shadow color only when both the r/g/b channels AND
831
- // the alpha match a sidecar shadow token's color. Alpha has to be compared
832
- // here because colorKey()/colorsClose() drop it, and a match on r/g/b alone
833
- // would let every black at every alpha through.
834
- function isAllowedShadowColorRaw(raw, designSystem) {
835
- if (!designSystem?.allowedShadowColors?.length) return false;
836
- const parsed = parseDesignColor(String(raw || '').trim().toLowerCase());
837
- if (!parsed) return false;
838
- return designSystem.allowedShadowColors.some(entry =>
839
- colorsClose(parsed, entry.color) &&
840
- Math.abs((parsed.a ?? 1) - (entry.color.a ?? 1)) <= SHADOW_ALPHA_TOLERANCE,
841
- );
842
- }
843
-
844
- function isAllowedRadiusRaw(raw, designSystem) {
845
- if (!designSystem?.hasRadii) return true;
846
- const text = String(raw || '').trim().toLowerCase();
847
- if (!text || text === '0' || text === 'none' || text === 'initial' || text === 'inherit') return true;
848
- if (text.includes('var(') || text.includes('%')) return true;
849
- const px = resolveLengthPx(text, 16);
850
- if (px == null || !Number.isFinite(px) || px <= RADIUS_TOLERANCE_PX) return true;
851
- if (designSystem.hasPillRadius && px >= 99) return true;
852
- return designSystem.allowedRadii.some(entry => Math.abs(entry.px - px) <= RADIUS_TOLERANCE_PX);
853
- }
854
-
855
- // One term of a font-size value. `unjudgeable` covers var(), calc(), percentages
856
- // and units the ramp cannot resolve (em is parent-relative, not root-relative);
857
- // those abstain rather than guess.
858
- function fontSizeStepStatus(raw, designSystem) {
859
- const text = String(raw || '').trim().toLowerCase();
860
- if (!FONT_SIZE_LITERAL_RE.test(text)) return 'unjudgeable';
861
- const px = resolveLengthPx(text, 16);
862
- if (px == null || !Number.isFinite(px) || px <= 0) return 'unjudgeable';
863
- return designSystem.allowedFontSizes.some(
864
- entry => Math.abs(entry.px - px) <= FONT_SIZE_TOLERANCE_PX,
865
- ) ? 'on-ramp' : 'off-ramp';
866
- }
867
-
868
- // The off-ramp endpoints of a fluid value, or null when `raw` is not a fluid
869
- // value at all. Only the min and max are judged: the viewport term interpolates
870
- // between them and is never a fixed step.
871
- //
872
- // Reading clamp endpoints as documented steps without also checking them in
873
- // usage would let `clamp(99rem, 1vw, 200rem)` through, which is how a fluid
874
- // declaration stayed invisible until someone measured computed styles.
875
- export function offRampClampEndpoints(raw, designSystem) {
876
- if (!designSystem?.hasFontSizes) return null;
877
- const args = parseClampArgs(String(raw || '').trim().replace(/\s*!important\s*$/i, ''));
878
- if (!args) return null;
879
- return [args[0], args[2]].filter(
880
- endpoint => fontSizeStepStatus(endpoint, designSystem) === 'off-ramp',
881
- );
882
- }
883
-
884
- function isAllowedFontSizeRaw(raw, designSystem) {
885
- if (!designSystem?.hasFontSizes) return true;
886
- const text = String(raw || '').trim().toLowerCase().replace(/\s*!important\s*$/, '');
887
- const offRampEndpoints = offRampClampEndpoints(text, designSystem);
888
- if (offRampEndpoints) return offRampEndpoints.length === 0;
889
- return fontSizeStepStatus(text, designSystem) !== 'off-ramp';
890
- }
891
-
892
- function lineLooksCommented(line) {
893
- const trimmed = String(line || '').trim();
894
- return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('<!--');
895
- }
896
-
897
- function isProbablyColorLiteral(line, match) {
898
- const raw = match?.[0] || '';
899
- const index = match.index ?? -1;
900
- if (index < 0) return false;
901
- if (isInsideCssAttributeSelector(line, index)) return false;
902
-
903
- const before = line.slice(0, index);
904
- const after = line.slice(index + raw.length);
905
-
906
- if (raw.startsWith('#')) {
907
- if (before.endsWith('&')) return false; // HTML numeric entity, e.g. &#8596;
908
-
909
- const prevNonSpace = before.match(/\S(?=\s*$)/)?.[0] || '';
910
- const nextNonSpace = after.match(/^\s*(\S)/)?.[1] || '';
911
- if (prevNonSpace === '>' && nextNonSpace === '<') return false; // plain text, e.g. PR #155
912
- }
913
-
914
- const styleContext = /(?:^|[{\s;"'`(,])(?:color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left))?(?:-color)?|outline(?:-color)?|box-shadow|text-shadow|fill|stroke)\s*:\s*[^;{}"'`]*/i.test(before);
915
- const cssFunctionContext = /(?:linear-gradient|radial-gradient|conic-gradient|color-mix)\([^)]*$/i.test(before);
916
- const jsColorKeyContext = /(?:^|[,{]\s*)(?:color|background|backgroundColor|borderColor|outlineColor|fill|stroke|boxShadow|textShadow)\s*[:=]\s*["'`]?[^"'`,}]*/i.test(before);
917
-
918
- return styleContext || cssFunctionContext || jsColorKeyContext;
919
- }
920
-
921
- // One complete `${...}` template interpolation. Its content may carry paired
922
- // quoted strings (function arguments, ternary branches) and one level of
923
- // braces (an object-literal argument, itself allowing paired quotes). Deeper
924
- // nesting would need a parser, so the regex deliberately fails safe there:
925
- // the context check misses and the finding fires — a false positive a waiver
926
- // can silence, never a leak.
927
- const QUOTED_STRING_SRC = `"[^"]*"|'[^']*'`;
928
- const INTERPOLATION_SRC =
929
- `\\$\\{(?:${QUOTED_STRING_SRC}|\\{(?:${QUOTED_STRING_SRC}|[^{}"'\`])*\\}|[^{}"'\`])*\\}`;
930
- // The two shadow-context tails. Unlike jsColorKeyContext, the JS tail admits
931
- // commas: a multi-layer shadow string is comma-separated, and a later
932
- // property on the same line is still blocked because it sits past the
933
- // string's closing quote. Both tails admit complete interpolations; a bare
934
- // `}`, quote, or `;` still ends the context.
935
- const SHADOW_CSS_CONTEXT_RE = new RegExp(
936
- `(?:^|[{\\s;"'\`(,])(?:box-shadow|text-shadow)\\s*:\\s*(?:${INTERPOLATION_SRC}|[^;{}"'\`])*$`, 'i',
937
- );
938
- const SHADOW_JS_CONTEXT_RE = new RegExp(
939
- `(?:^|[,{]\\s*)(?:boxShadow|textShadow)\\s*[:=]\\s*["'\`]?(?:${INTERPOLATION_SRC}|[^"'\`}])*$`, 'i',
940
- );
941
-
942
- // True when the color literal sits inside a box-shadow / text-shadow value —
943
- // the only contexts where a documented shadow color is legal. Anchored to the
944
- // end of `before` (no ; } { or quote in between) so a shadow property earlier
945
- // on the line can't leak the allowance into a later declaration. Kept separate
946
- // from isProbablyColorLiteral(), which stays a boolean for its existing call
947
- // sites and deliberately discards which property matched.
948
- function isShadowPropertyContext(line, match) {
949
- const index = match.index ?? -1;
950
- if (index < 0) return false;
951
- const before = line.slice(0, index);
952
- return SHADOW_CSS_CONTEXT_RE.test(before) || SHADOW_JS_CONTEXT_RE.test(before);
953
- }
954
-
955
- function isInsideCssAttributeSelector(line, index) {
956
- if (index < 0) return false;
957
- const before = line.slice(0, index);
958
- const lastOpen = before.lastIndexOf('[');
959
- if (lastOpen === -1) return false;
960
- const lastClose = before.lastIndexOf(']');
961
- if (lastClose > lastOpen) return false;
962
- const after = line.slice(index);
963
- const close = after.indexOf(']');
964
- const block = after.indexOf('{');
965
- return close !== -1 && (block === -1 || close < block);
966
- }
967
-
968
- function makeDesignFinding(id, filePath, snippet, line = 0, extras = {}) {
969
- return { ...finding(id, filePath, snippet, line), ...extras };
970
- }
971
-
972
- function decodeGoogleFamily(value) {
973
- const family = String(value || '').split(':')[0].replace(/\+/g, ' ');
974
- try {
975
- return decodeURIComponent(family);
976
- } catch {
977
- return family;
978
- }
979
- }
980
-
981
- function checkFontStack(stack, filePath, line, designSystem, context) {
982
- const primary = primaryFont(stack);
983
- if (!primary || isAllowedFont(primary, designSystem)) return [];
984
- const display = primary.replace(/\b\w/g, ch => ch.toUpperCase());
985
- return [makeDesignFinding(
986
- 'design-system-font',
987
- filePath,
988
- `${context}: ${display} is not declared in DESIGN.md typography`,
989
- line,
990
- { ignoreValue: display },
991
- )];
992
- }
993
-
994
- function extractRadiusTokens(value) {
995
- return String(value || '')
996
- .replace(/\s*\/\s*/g, ' ')
997
- .split(/\s+/)
998
- // var() fallbacks leave the closing parenthesis on the final token. Strip
999
- // it before length resolution so `8px)` is not treated as unitless 8rem.
1000
- .map(token => token.trim().replace(/\)+$/, ''))
1001
- .filter(Boolean);
1002
- }
1003
-
1004
- function checkRadiusValue(value, filePath, line, designSystem, context) {
1005
- const findings = [];
1006
- for (const token of extractRadiusTokens(value)) {
1007
- if (isAllowedRadiusRaw(token, designSystem)) continue;
1008
- findings.push(makeDesignFinding(
1009
- 'design-system-radius',
1010
- filePath,
1011
- `${context}: ${token} is outside the DESIGN.md rounded scale`,
1012
- line,
1013
- { ignoreValue: token },
1014
- ));
1015
- }
1016
- return findings;
1017
- }
1018
-
1019
- function checkFontSizeValue(value, filePath, line, designSystem, context) {
1020
- const token = String(value || '').trim();
1021
- if (isAllowedFontSizeRaw(token, designSystem)) return [];
1022
-
1023
- // Name the offending endpoint on a fluid value; the whole clamp() string is
1024
- // not actionable on its own, and it makes a poor ignore-value.
1025
- const offRampEndpoints = offRampClampEndpoints(token, designSystem) || [];
1026
- if (offRampEndpoints.length > 0) {
1027
- const plural = offRampEndpoints.length > 1 ? 's' : '';
1028
- return [makeDesignFinding(
1029
- 'design-system-font-size',
1030
- filePath,
1031
- `${context}: ${token} has fluid endpoint${plural} ${offRampEndpoints.join(' and ')} off the DESIGN.md type ramp`,
1032
- line,
1033
- { ignoreValue: offRampEndpoints[0] },
1034
- )];
1035
- }
1036
-
1037
- // The snippet shows the declaration as authored, but the ignoreValue has to
1038
- // be what a `hooks ignore-value` waiver can match, so the priority marker is
1039
- // stripped. Otherwise the same size needs two different waivers depending on
1040
- // whether it carries !important. font-family already behaves this way.
1041
- return [makeDesignFinding(
1042
- 'design-system-font-size',
1043
- filePath,
1044
- `${context}: ${token} is off the DESIGN.md type ramp`,
1045
- line,
1046
- { ignoreValue: token.replace(/\s*!important\s*$/i, '').trim() },
1047
- )];
1048
- }
1049
-
1050
- function checkSourceDesignSystem(content, filePath, options = {}) {
1051
- const designSystem = options.designSystem;
1052
- if (!designSystem?.present) return [];
1053
-
1054
- const findings = [];
1055
- const lines = String(content || '').split('\n');
1056
- for (let i = 0; i < lines.length; i++) {
1057
- const line = lines[i];
1058
- const lineNum = i + 1;
1059
- if (lineLooksCommented(line)) continue;
1060
-
1061
- if (designSystem.hasFonts) {
1062
- for (const match of line.matchAll(FONT_DECL_RE)) {
1063
- findings.push(...checkFontStack(match[1], filePath, lineNum, designSystem, 'font-family'));
1064
- }
1065
- for (const match of line.matchAll(FONT_JS_RE)) {
1066
- findings.push(...checkFontStack(match[1], filePath, lineNum, designSystem, 'fontFamily'));
1067
- }
1068
- for (const match of line.matchAll(GOOGLE_FONT_RE)) {
1069
- const url = match[0];
1070
- for (const familyMatch of url.matchAll(/[?&]family=([^&]+)/g)) {
1071
- const font = normalizeFontName(decodeGoogleFamily(familyMatch[1]));
1072
- if (!font || isAllowedFont(font, designSystem)) continue;
1073
- const display = decodeGoogleFamily(familyMatch[1]);
1074
- findings.push(makeDesignFinding(
1075
- 'design-system-font',
1076
- filePath,
1077
- `Google Fonts: ${display} is not declared in DESIGN.md typography`,
1078
- lineNum,
1079
- { ignoreValue: display },
1080
- ));
1081
- }
1082
- }
1083
- }
1084
-
1085
- if (designSystem.hasColors) {
1086
- for (const match of line.matchAll(CSS_COLOR_RE)) {
1087
- if (!isProbablyColorLiteral(line, match)) continue;
1088
- const raw = cssColorLabel(match[0]);
1089
- if (isAllowedColorRaw(raw, designSystem)) continue;
1090
- if (isShadowPropertyContext(line, match) && isAllowedShadowColorRaw(raw, designSystem)) continue;
1091
- findings.push(makeDesignFinding(
1092
- 'design-system-color',
1093
- filePath,
1094
- `Undocumented color ${raw} is outside DESIGN.md colors`,
1095
- lineNum,
1096
- { ignoreValue: raw },
1097
- ));
1098
- }
1099
- }
1100
-
1101
- if (designSystem.hasRadii) {
1102
- for (const match of line.matchAll(BORDER_RADIUS_RE)) {
1103
- findings.push(...checkRadiusValue(match[1], filePath, lineNum, designSystem, 'border-radius'));
1104
- }
1105
- for (const match of line.matchAll(BORDER_RADIUS_JS_RE)) {
1106
- findings.push(...checkRadiusValue(match[1], filePath, lineNum, designSystem, 'borderRadius'));
1107
- }
1108
- }
1109
-
1110
- if (designSystem.hasFontSizes) {
1111
- for (const match of line.matchAll(FONT_SIZE_DECL_RE)) {
1112
- findings.push(...checkFontSizeValue(match[1], filePath, lineNum, designSystem, 'font-size'));
1113
- }
1114
- for (const match of line.matchAll(FONT_SIZE_JS_RE)) {
1115
- findings.push(...checkFontSizeValue(match[1], filePath, lineNum, designSystem, 'fontSize'));
1116
- }
1117
- for (const match of line.matchAll(TAILWIND_FONT_SIZE_RE)) {
1118
- findings.push(...checkFontSizeValue(match[1], filePath, lineNum, designSystem, 'text-[…] class'));
1119
- }
1120
- }
1121
- }
1122
-
1123
- return dedupeDesignFindings(findings);
1124
- }
1125
-
1126
- function hasDirectText(el) {
1127
- return Array.from(el.childNodes || []).some(node => node.nodeType === 3 && node.textContent.trim().length > 0);
1128
- }
1129
-
1130
- function sampleText(el) {
1131
- const text = String(el.textContent || '').replace(/\s+/g, ' ').trim();
1132
- return text ? ` "${text.slice(0, 40)}"` : '';
1133
- }
1134
-
1135
- // Font-size design-system checks are source-scan-only (see checkSourceDesignSystem).
1136
- // Computed font-size cascades and clamp() ramps resolve to off-ramp px in the browser.
1137
- function collectStaticDesignSystemFindings(document, window, filePath, designSystem) {
1138
- if (!designSystem?.present) return [];
1139
- const findings = [];
1140
- const seenFonts = new Set();
1141
- const seenColors = new Set();
1142
- const seenRadii = new Set();
1143
-
1144
- for (const el of document.querySelectorAll('*')) {
1145
- if (shouldSkipStaticDesignElement(el, window)) continue;
1146
- const tag = el.tagName?.toLowerCase?.() || 'unknown';
1147
- const style = window.getComputedStyle(el);
1148
-
1149
- if (designSystem.hasFonts && hasDirectText(el)) {
1150
- const font = primaryFont(style.fontFamily || '');
1151
- if (font && !seenFonts.has(font) && !isAllowedFont(font, designSystem)) {
1152
- seenFonts.add(font);
1153
- findings.push(makeDesignFinding(
1154
- 'design-system-font',
1155
- filePath,
1156
- `${tag}${sampleText(el)} uses ${font}; not declared in DESIGN.md typography`,
1157
- 0,
1158
- { ignoreValue: font },
1159
- ));
1160
- }
1161
- }
1162
-
1163
- if (designSystem.hasColors) {
1164
- const colorChecks = [];
1165
- if (hasDirectText(el)) colorChecks.push(['text color', style.color]);
1166
- if (!isTransparentCss(style.backgroundColor)) colorChecks.push(['background', style.backgroundColor]);
1167
- for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
1168
- if ((parseFloat(style[`border${side}Width`]) || 0) > 0) {
1169
- colorChecks.push([`border-${side.toLowerCase()}`, style[`border${side}Color`]]);
1170
- }
1171
- }
1172
- if ((parseFloat(style.outlineWidth) || 0) > 0) colorChecks.push(['outline', style.outlineColor]);
1173
-
1174
- for (const [kind, raw] of colorChecks) {
1175
- const label = cssColorLabel(raw);
1176
- if (isAllowedColorRaw(label, designSystem)) continue;
1177
- const key = `${kind}:${label}`;
1178
- if (seenColors.has(key)) continue;
1179
- seenColors.add(key);
1180
- findings.push(makeDesignFinding(
1181
- 'design-system-color',
1182
- filePath,
1183
- `${kind} ${label} on ${tag}${sampleText(el)} is outside DESIGN.md colors`,
1184
- 0,
1185
- { ignoreValue: label },
1186
- ));
1187
- }
1188
- }
1189
-
1190
- if (designSystem.hasRadii) {
1191
- const rawRadius = String(style.borderRadius || '').trim();
1192
- if (!rawRadius) continue;
1193
- for (const token of extractRadiusTokens(rawRadius)) {
1194
- if (isAllowedRadiusRaw(token, designSystem)) continue;
1195
- if (seenRadii.has(token)) continue;
1196
- seenRadii.add(token);
1197
- findings.push(makeDesignFinding(
1198
- 'design-system-radius',
1199
- filePath,
1200
- `border-radius ${token} on ${tag}${sampleText(el)} is outside the DESIGN.md rounded scale`,
1201
- 0,
1202
- { ignoreValue: token },
1203
- ));
1204
- }
1205
- }
1206
- }
1207
-
1208
- return findings;
1209
- }
1210
-
1211
- function shouldSkipStaticDesignElement(el, window) {
1212
- const tag = el.tagName?.toLowerCase?.() || '';
1213
- if (STATIC_DESIGN_SKIP_TAGS.has(tag)) return true;
1214
-
1215
- let current = el;
1216
- while (current) {
1217
- if (current.getAttribute?.('hidden') !== null || current.getAttribute?.('aria-hidden') === 'true') return true;
1218
- const style = window.getComputedStyle(current);
1219
- const display = String(style.display || '').toLowerCase();
1220
- const visibility = String(style.visibility || '').toLowerCase();
1221
- if (display === 'none' || visibility === 'hidden' || visibility === 'collapse') return true;
1222
- current = current.parentElement;
1223
- }
1224
- return false;
1225
- }
1226
-
1227
- function isTransparentCss(value) {
1228
- const text = String(value || '').trim().toLowerCase();
1229
- if (!text || text === 'transparent') return true;
1230
- const parsed = parseDesignColor(text);
1231
- return parsed ? (parsed.a ?? 1) <= 0.05 : false;
1232
- }
1233
-
1234
- function canonicalDesignFindingKey(item) {
1235
- if (!item?.antipattern?.startsWith?.('design-system-')) return null;
1236
- const value = item.ignoreValue || item.value || '';
1237
- if (item.antipattern === 'design-system-font') {
1238
- const context = /google fonts/i.test(item.snippet || '') ? 'google-font' : 'font';
1239
- const font = normalizeFontName(value);
1240
- return font ? `${item.antipattern}:${context}:${font}` : null;
1241
- }
1242
- if (item.antipattern === 'design-system-color') {
1243
- const parsed = parseDesignColor(value);
1244
- if (parsed) return `${item.antipattern}:color:${colorKey(parsed)}`;
1245
- const label = cssColorLabel(value).toLowerCase();
1246
- return label ? `${item.antipattern}:color:${label}` : null;
1247
- }
1248
- if (item.antipattern === 'design-system-radius') {
1249
- const px = resolveLengthPx(String(value || '').trim(), 16);
1250
- if (px != null && Number.isFinite(px)) return `${item.antipattern}:radius:${Math.round(px * 100) / 100}`;
1251
- const label = String(value || '').trim().toLowerCase();
1252
- return label ? `${item.antipattern}:radius:${label}` : null;
1253
- }
1254
- if (item.antipattern === 'design-system-font-size') {
1255
- const px = resolveLengthPx(String(value || '').trim(), 16);
1256
- if (px != null && Number.isFinite(px)) return `${item.antipattern}:font-size:${Math.round(px * 100) / 100}`;
1257
- const label = String(value || '').trim().toLowerCase();
1258
- return label ? `${item.antipattern}:font-size:${label}` : null;
1259
- }
1260
- return null;
1261
- }
1262
-
1263
- function mergeDesignSystemFindings(...groups) {
1264
- const out = [];
1265
- const seen = new Map();
1266
- for (const group of groups) {
1267
- for (const item of group || []) {
1268
- const key = canonicalDesignFindingKey(item);
1269
- if (key) {
1270
- if (seen.has(key)) {
1271
- const existing = out[seen.get(key)];
1272
- if ((existing.line || 0) <= 0 && (item.line || 0) > 0) existing.line = item.line;
1273
- continue;
1274
- }
1275
- seen.set(key, out.length);
1276
- }
1277
- out.push(item);
1278
- }
1279
- }
1280
- return out;
1281
- }
1282
-
1283
- function dedupeDesignFindings(findings) {
1284
- const out = [];
1285
- const seen = new Set();
1286
- for (const item of findings) {
1287
- const key = [
1288
- item.antipattern,
1289
- item.line || 0,
1290
- normalizeFontName(item.ignoreValue || item.snippet || ''),
1291
- ].join('\0');
1292
- if (seen.has(key)) continue;
1293
- seen.add(key);
1294
- out.push(item);
1295
- }
1296
- return out;
1297
- }
1298
-
1299
- export {
1300
- parseFrontmatter,
1301
- normalizeDesignSystem,
1302
- loadDesignSystemForCwd,
1303
- isAllowedFont,
1304
- isAllowedColorRaw,
1305
- isAllowedShadowColorRaw,
1306
- isAllowedRadiusRaw,
1307
- isAllowedFontSizeRaw,
1308
- checkSourceDesignSystem,
1309
- collectStaticDesignSystemFindings,
1310
- mergeDesignSystemFindings,
1311
- };