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,1319 +0,0 @@
1
- import { GENERIC_FONTS, OVERUSED_FONTS, EM_DASH_FLOOR, EM_DASH_CHARS_PER_DASH } from '../../shared/constants.mjs';
2
- import { isNeutralColor } from '../../shared/color.mjs';
3
- import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
4
- import { checkSourceDesignSystem } from '../../design-system.mjs';
5
- import { scanCssTextForGlow, scanCssTextForGridBackground, scanCssTextForMarquee, scanCssTextForPseudoStripe, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
6
- import { isFullPage } from '../../shared/page.mjs';
7
- import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
8
- import { finding } from '../../findings.mjs';
9
- import { profileFindings, profileStep } from '../../profile/profiler.mjs';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Regex fallback (non-HTML files: CSS, JSX, TSX, etc.)
13
- // ---------------------------------------------------------------------------
14
-
15
- const hasRounded = (line) =>
16
- /\brounded(?:-\w+)?\b/.test(line.replace(/\brounded-none\b/g, ''));
17
- const hasBorderRadius = (line) => /border-radius/i.test(line);
18
- const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|input[\s>]|span[\s>])/i.test(line);
19
-
20
-
21
- /** Strip HTML to plain text — drops script/style/comments/tags so
22
- * content-text analyzers don't false-positive on code or CSS. */
23
- function stripHtmlToText(html) {
24
- return html
25
- .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
26
- .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
27
- .replace(/<!--[\s\S]*?-->/g, ' ')
28
- .replace(/<[^>]+>/g, ' ')
29
- .replace(/\s+/g, ' ');
30
- }
31
-
32
- const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
33
-
34
- function extFromFilePath(filePath) {
35
- return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
36
- }
37
-
38
- function shouldRunPageAnalyzers(content, filePath) {
39
- if (!isFullPage(content)) return false;
40
- const ext = extFromFilePath(filePath);
41
- return !ext || PAGE_ANALYZER_EXTS.has(ext);
42
- }
43
-
44
- const JS_SOURCE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
45
- const STYLESHEET_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
46
- const REGEX_PREFIX_KEYWORDS = new Set(['await', 'case', 'default', 'delete', 'do', 'else', 'in', 'instanceof', 'new', 'of', 'return', 'throw', 'typeof', 'void', 'yield']);
47
- const BLOCK_BRACE_PREFIX_KEYWORDS = new Set(['do', 'else', 'finally', 'try']);
48
-
49
- function isInsideOpeningJsxTag(source) {
50
- const tagStart = source.lastIndexOf('<');
51
- if (tagStart === -1 || !/^<[A-Za-z][\w.:-]*/.test(source.slice(tagStart))) return false;
52
-
53
- let quote = '';
54
- for (let cursor = tagStart + 1; cursor < source.length; cursor++) {
55
- const char = source[cursor];
56
- if (quote) {
57
- if (char === '\\') cursor++;
58
- else if (char === quote) quote = '';
59
- } else if (char === "'" || char === '"') {
60
- quote = char;
61
- } else if (char === '>') {
62
- return false;
63
- }
64
- }
65
- return true;
66
- }
67
-
68
- /**
69
- * Blank JavaScript comments without moving any following source. Regex
70
- * findings keep their original line numbers, while prose examples inside
71
- * comments cannot masquerade as rendered markup.
72
- */
73
- function stripJsComments(content, options = {}) {
74
- let state = 'code';
75
- let output = '';
76
- let lastSignificant = '';
77
- let previousSignificant = '';
78
- let antePreviousSignificant = '';
79
- let currentWord = '';
80
- let currentWordPrefix = '';
81
- let wordSeparated = false;
82
- let regexCharClass = false;
83
- let jsxExpressionDepth = 0;
84
- let lastClosedBraceKind = '';
85
- const braceKinds = [];
86
- const templateExpressionDepths = [];
87
-
88
- const braceKind = (startsJsxExpression = false) => (
89
- !startsJsxExpression && (
90
- !lastSignificant ||
91
- lastSignificant === ')' ||
92
- lastSignificant === ';' ||
93
- lastSignificant === '}' ||
94
- (previousSignificant === '=' && lastSignificant === '>') ||
95
- BLOCK_BRACE_PREFIX_KEYWORDS.has(currentWord)
96
- ) ? 'block' : 'expression'
97
- );
98
-
99
- const recordSignificant = (char) => {
100
- if (/\s/.test(char)) {
101
- wordSeparated = true;
102
- return;
103
- }
104
- const isWordChar = /[\w$]/.test(char);
105
- if (isWordChar && (wordSeparated || !currentWord)) {
106
- currentWord = '';
107
- currentWordPrefix = lastSignificant;
108
- } else if (!isWordChar) {
109
- currentWordPrefix = '';
110
- }
111
- wordSeparated = false;
112
- antePreviousSignificant = previousSignificant;
113
- previousSignificant = lastSignificant;
114
- lastSignificant = char;
115
- currentWord = isWordChar ? currentWord + char : '';
116
- };
117
-
118
- for (let i = 0; i < content.length; i++) {
119
- const char = content[i];
120
- const next = content[i + 1];
121
-
122
- if (state === 'line-comment') {
123
- if (char === '\n') {
124
- output += char;
125
- state = 'code';
126
- } else {
127
- output += ' ';
128
- }
129
- continue;
130
- }
131
-
132
- if (state === 'block-comment') {
133
- if (char === '*' && next === '/') {
134
- output += ' ';
135
- i++;
136
- state = 'code';
137
- } else {
138
- output += char === '\n' ? '\n' : ' ';
139
- }
140
- continue;
141
- }
142
-
143
- if (state === 'regex') {
144
- output += char;
145
- if (char === '\\' && next) {
146
- output += next;
147
- i++;
148
- } else if (char === '[') {
149
- regexCharClass = true;
150
- } else if (char === ']') {
151
- regexCharClass = false;
152
- } else if (char === '/' && !regexCharClass) {
153
- state = 'code';
154
- recordSignificant('/');
155
- }
156
- continue;
157
- }
158
-
159
- if (state === 'template' && char === '$' && next === '{') {
160
- output += '${';
161
- i++;
162
- recordSignificant('$');
163
- recordSignificant('{');
164
- templateExpressionDepths.push(1);
165
- braceKinds.push('expression');
166
- if (jsxExpressionDepth) jsxExpressionDepth++;
167
- state = 'code';
168
- continue;
169
- }
170
-
171
- if (state !== 'code') {
172
- output += char;
173
- if (char === '\\' && next) {
174
- output += next;
175
- i++;
176
- } else if (
177
- (state === 'single-quote' && char === "'") ||
178
- (state === 'double-quote' && char === '"') ||
179
- (state === 'template' && char === '`')
180
- ) {
181
- state = 'code';
182
- recordSignificant(char);
183
- }
184
- continue;
185
- }
186
-
187
- const jsxUrlSeparator = options.jsx && char === '/' && next === '/' &&
188
- jsxExpressionDepth === 0 &&
189
- (output.endsWith('http:') ||
190
- output.endsWith('https:') ||
191
- (/<[A-Za-z](?:[^>]*[^/])?>[^<]*$/.test(output.slice(output.lastIndexOf('\n') + 1)) &&
192
- /^[\w.-]+\.[A-Za-z]{2,}(?=[:/?#\s<]|$)/.test(content.slice(i + 2))));
193
- const afterPostfixUpdate = (lastSignificant === '+' || lastSignificant === '-') &&
194
- previousSignificant === lastSignificant &&
195
- antePreviousSignificant !== lastSignificant;
196
- if (char === '/' && next === '/' && jsxUrlSeparator) {
197
- output += '//';
198
- i++;
199
- recordSignificant('/');
200
- recordSignificant('/');
201
- } else if (char === '/' && next === '/') {
202
- output += ' ';
203
- i++;
204
- state = 'line-comment';
205
- } else if (char === '/' && next === '*') {
206
- output += ' ';
207
- i++;
208
- state = 'block-comment';
209
- } else if (templateExpressionDepths.length && char === '{') {
210
- output += char;
211
- templateExpressionDepths[templateExpressionDepths.length - 1]++;
212
- braceKinds.push(braceKind());
213
- if (jsxExpressionDepth) jsxExpressionDepth++;
214
- recordSignificant(char);
215
- } else if (templateExpressionDepths.length && char === '}') {
216
- output += char;
217
- const depthIndex = templateExpressionDepths.length - 1;
218
- templateExpressionDepths[depthIndex]--;
219
- lastClosedBraceKind = braceKinds.pop() || '';
220
- if (jsxExpressionDepth) jsxExpressionDepth--;
221
- recordSignificant(char);
222
- if (templateExpressionDepths[depthIndex] === 0) {
223
- templateExpressionDepths.pop();
224
- state = 'template';
225
- }
226
- } else if (
227
- char === '/' &&
228
- (!lastSignificant ||
229
- (/[=([{!?:;,&|+\-*%^~<>]/.test(lastSignificant) && !afterPostfixUpdate) ||
230
- (lastSignificant === '}' && lastClosedBraceKind === 'block') ||
231
- (previousSignificant === '=' && lastSignificant === '>') ||
232
- (currentWordPrefix !== '.' && REGEX_PREFIX_KEYWORDS.has(currentWord)))
233
- ) {
234
- output += char;
235
- state = 'regex';
236
- regexCharClass = false;
237
- } else {
238
- output += char;
239
- const startsJsxExpression = options.jsx && char === '{' && jsxExpressionDepth === 0 &&
240
- (/<[A-Za-z](?:[^>]*[^/])?>[^<]*$/.test(output.slice(output.lastIndexOf('\n') + 1, -1)) ||
241
- isInsideOpeningJsxTag(output.slice(0, -1)));
242
- if (char === '{') braceKinds.push(braceKind(startsJsxExpression));
243
- else if (char === '}') lastClosedBraceKind = braceKinds.pop() || '';
244
- if (char === '{' && (jsxExpressionDepth || startsJsxExpression)) jsxExpressionDepth++;
245
- else if (char === '}' && jsxExpressionDepth) jsxExpressionDepth--;
246
- recordSignificant(char);
247
- if (char === "'") state = 'single-quote';
248
- else if (char === '"') state = 'double-quote';
249
- else if (char === '`') state = 'template';
250
- }
251
- }
252
-
253
- return output;
254
- }
255
-
256
- function stripCssComments(content) {
257
- return content.replace(/\/\*[\s\S]*?\*\//g, comment => comment.replace(/[^\n]/g, ' '));
258
- }
259
-
260
- function blankHtmlComments(text) {
261
- return text.replace(/<!--[\s\S]*?-->/g, comment => comment.replace(/[^\n]/g, ' '));
262
- }
263
-
264
- function blankCssLineCommentsInStyleBlocks(text) {
265
- const re = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
266
- let output = '';
267
- let lastIndex = 0;
268
- let match;
269
- while ((match = re.exec(text)) !== null) {
270
- const inner = match[1];
271
- const openLength = match[0].length - inner.length - '</style>'.length;
272
- output += text.slice(lastIndex, match.index);
273
- output += match[0].slice(0, openLength);
274
- output += blankCssLineComments(inner);
275
- output += match[0].slice(openLength + inner.length);
276
- lastIndex = re.lastIndex;
277
- }
278
- return output + text.slice(lastIndex);
279
- }
280
-
281
- function blankHtmlAndCssCommentsOutsideScripts(text) {
282
- const re = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
283
- let output = '';
284
- let lastIndex = 0;
285
- let match;
286
- while ((match = re.exec(text)) !== null) {
287
- output += blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex, match.index))));
288
- output += match[0];
289
- lastIndex = re.lastIndex;
290
- }
291
- return output + blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex))));
292
- }
293
-
294
- function blankCssLineComments(text) {
295
- let output = '';
296
- let state = 'code';
297
- let urlDepth = 0;
298
- for (let i = 0; i < text.length; i++) {
299
- const char = text[i];
300
- const next = text[i + 1];
301
- if (state === 'line') {
302
- if (char === '\n') {
303
- output += '\n';
304
- state = 'code';
305
- } else {
306
- output += ' ';
307
- }
308
- continue;
309
- }
310
- if (state === 'single' || state === 'double') {
311
- output += char;
312
- if (char === '\\' && next) {
313
- output += next;
314
- i++;
315
- } else if ((state === 'single' && char === "'") || (state === 'double' && char === '"')) {
316
- state = 'code';
317
- }
318
- continue;
319
- }
320
- const prev = output.length ? output[output.length - 1] : '';
321
- if (char === '/' && next === '/' && urlDepth === 0 && prev !== ':' && prev !== '(' && prev !== '\\') {
322
- output += ' ';
323
- i++;
324
- state = 'line';
325
- continue;
326
- }
327
- if (char === "'") state = 'single';
328
- else if (char === '"') state = 'double';
329
- if (char === '(') {
330
- const behind = output.replace(/\s+$/, '');
331
- if (urlDepth > 0 || /url$/i.test(behind)) urlDepth++;
332
- } else if (char === ')' && urlDepth) {
333
- urlDepth--;
334
- }
335
- output += char;
336
- }
337
- return output;
338
- }
339
-
340
- function findAstroFrontmatterClose(text) {
341
- if (!text.startsWith('---')) return -1;
342
- let cursor = text.indexOf('\n');
343
- if (cursor === -1) return -1;
344
- cursor += 1;
345
- while (cursor < text.length) {
346
- if (text[cursor - 1] === '\n' && text.startsWith('---', cursor)) {
347
- let end = cursor + 3;
348
- while (text[end] === ' ' || text[end] === '\t') end++;
349
- if (end >= text.length || text[end] === '\n' || text[end] === '\r') return cursor - 1;
350
- }
351
- const char = text[cursor];
352
- const next = text[cursor + 1];
353
- if (char === "'" || char === '"') {
354
- const close = findQuotedStringEnd(text, cursor, char);
355
- if (close === -1) return -1;
356
- cursor = close + 1;
357
- continue;
358
- }
359
- if (char === '`') {
360
- const close = findTemplateLiteralEnd(text, cursor);
361
- if (close === -1) return -1;
362
- cursor = close + 1;
363
- continue;
364
- }
365
- if (char === '/' && next === '/') {
366
- const lineEnd = text.indexOf('\n', cursor);
367
- if (lineEnd === -1) return -1;
368
- cursor = lineEnd;
369
- continue;
370
- }
371
- if (char === '/' && next === '*') {
372
- const commentEnd = text.indexOf('*/', cursor + 2);
373
- if (commentEnd === -1) return -1;
374
- cursor = commentEnd + 2;
375
- continue;
376
- }
377
- if (char === '/' && next !== '/' && next !== '*') {
378
- const close = findRegexLiteralEnd(text, cursor);
379
- if (close !== -1) {
380
- cursor = close + 1;
381
- continue;
382
- }
383
- }
384
- cursor++;
385
- }
386
- return -1;
387
- }
388
-
389
- function blankAstroFrontmatterComments(text) {
390
- const close = findAstroFrontmatterClose(text);
391
- if (close === -1) return text;
392
- return stripJsComments(text.slice(0, close)) + text.slice(close);
393
- }
394
-
395
- function blankCommentsForMatchers(text, ext) {
396
- if (PAGE_ANALYZER_EXTS.has(ext)) {
397
- const withFrontmatter = ext === '.astro' ? blankAstroFrontmatterComments(text) : text;
398
- return blankHtmlAndCssCommentsOutsideScripts(withFrontmatter);
399
- }
400
- if (STYLESHEET_EXTS.has(ext)) {
401
- const withoutBlocks = stripCssComments(text);
402
- return ext === '.css' ? withoutBlocks : blankCssLineComments(withoutBlocks);
403
- }
404
- return text;
405
- }
406
-
407
- function firstOverusedGoogleFont(text) {
408
- return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
409
- }
410
-
411
- // CSS named colors whose channels are equal (achromatic). Anything outside
412
- // this set falls through to the format parsers, and an unrecognized spelling
413
- // stays non-neutral so a real accent is never skipped.
414
- const NEUTRAL_COLOR_KEYWORDS = new Set([
415
- 'transparent', 'currentcolor',
416
- 'black', 'white', 'gray', 'grey', 'silver',
417
- 'dimgray', 'dimgrey', 'darkgray', 'darkgrey', 'lightgray', 'lightgrey',
418
- 'gainsboro', 'whitesmoke',
419
- ]);
420
-
421
- function hexChannels(color) {
422
- const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
423
- if (long) return [parseInt(long[1], 16), parseInt(long[2], 16), parseInt(long[3], 16)];
424
- const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
425
- if (short) return [1, 2, 3].map((i) => parseInt(short[i] + short[i], 16));
426
- return null;
427
- }
428
-
429
- /**
430
- * Split one box-shadow layer into top-level tokens.
431
- *
432
- * Whitespace inside parens does not separate tokens: `rgb(0 0 0)` and
433
- * `var(--x, 4px)` are each a single value, and splitting them on spaces would
434
- * read their innards as separate lengths.
435
- */
436
- function tokenizeShadowLayer(layer) {
437
- const tokens = [];
438
- let depth = 0;
439
- let current = '';
440
- for (const char of String(layer || '')) {
441
- if (char === '(') depth++;
442
- else if (char === ')') depth--;
443
- else if (depth === 0 && /\s/.test(char)) {
444
- if (current) tokens.push(current);
445
- current = '';
446
- continue;
447
- }
448
- current += char;
449
- }
450
- if (current) tokens.push(current);
451
- return tokens;
452
- }
453
-
454
- function lastMatch(text, re) {
455
- const all = [...String(text || '').matchAll(re)];
456
- return all.length ? all[all.length - 1] : null;
457
- }
458
-
459
- function isShadowLength(token) {
460
- return /^-?\d*\.?\d+(?:px)?$/i.test(String(token || ''));
461
- }
462
-
463
- /**
464
- * Neutrality test for colors as written in source CSS.
465
- *
466
- * shared/color.mjs's isNeutralColor only parses the computed function forms a
467
- * browser or jsdom emits (rgb/oklch/lab/...) and deliberately reports every
468
- * other spelling as chromatic so an unknown format is never silently skipped.
469
- * That default is wrong for authored CSS, where `#000` and `black` are the
470
- * normal spellings: calling it directly reports a plain black hairline as a
471
- * colored stripe. Handle hex and named neutrals here, then defer.
472
- */
473
- function isNeutralAuthoredColor(rawColor) {
474
- const c = String(rawColor || '').trim().toLowerCase();
475
- if (!c) return false;
476
- if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
477
- // Modern rgb() takes space-separated channels (`rgb(0 0 0)`). shared/color.mjs
478
- // parses only the comma form a browser's getComputedStyle emits, so authored
479
- // space-separated neutrals fell through it and reported as chromatic — the
480
- // exemption this function exists for, missed. Normalize before delegating.
481
- if (/^rgba?\(/i.test(c)) {
482
- const channels = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
483
- if (channels) {
484
- const values = [1, 2, 3].map((i) => Number(channels[i]));
485
- return (Math.max(...values) - Math.min(...values)) < 30;
486
- }
487
- return isNeutralColor(c);
488
- }
489
- if (/^(?:hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
490
- const channels = hexChannels(c);
491
- if (channels) return (Math.max(...channels) - Math.min(...channels)) < 30;
492
- return false;
493
- }
494
-
495
- function isNeutralBorderColor(str) {
496
- const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
497
- if (!m) return false;
498
- return isNeutralAuthoredColor(m[1]);
499
- }
500
-
501
- const REGEX_MATCHERS = [
502
- // --- Side-tab ---
503
- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g,
504
- test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; },
505
- fmt: (m) => m[0] },
506
- { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi,
507
- test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; },
508
- fmt: (m) => m[0].replace(/\s*;?\s*$/, '') },
509
- { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi,
510
- test: (m, line) => !isSafeElement(line) && +m[1] >= 3,
511
- fmt: (m) => m[0] },
512
- { id: 'side-tab', regex: /border-inline-(?:start|end)\s*:\s*(\d+)px\s+solid/gi,
513
- test: (m, line) => !isSafeElement(line) && +m[1] >= 3,
514
- fmt: (m) => m[0] },
515
- { id: 'side-tab', regex: /border-inline-(?:start|end)-width\s*:\s*(\d+)px/gi,
516
- test: (m, line) => !isSafeElement(line) && +m[1] >= 3,
517
- fmt: (m) => m[0] },
518
- { id: 'side-tab', regex: /border(?:Left|Right)\s*[:=]\s*["'`](\d+)px\s+solid/g,
519
- test: (m) => +m[1] >= 3,
520
- fmt: (m) => m[0] },
521
- // --- Border accent on rounded ---
522
- { id: 'border-accent-on-rounded', regex: /\bborder-[tb]-(\d+)\b/g,
523
- test: (m, line) => hasRounded(line) && +m[1] >= 1,
524
- fmt: (m) => m[0] },
525
- { id: 'border-accent-on-rounded', regex: /border-(?:top|bottom)\s*:\s*(\d+)px\s+solid/gi,
526
- test: (m, line) => +m[1] >= 3 && hasBorderRadius(line),
527
- fmt: (m) => m[0] },
528
- // --- Overused font ---
529
- { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi,
530
- test: () => true,
531
- fmt: (m) => m[0] },
532
- { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi,
533
- test: (m) => {
534
- m.overusedGoogleFont = firstOverusedGoogleFont(m[0]);
535
- return Boolean(m.overusedGoogleFont);
536
- },
537
- fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` },
538
- // --- Gradient text ---
539
- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi,
540
- test: (m, line) => /gradient/i.test(line),
541
- fmt: () => 'background-clip: text + gradient' },
542
- // --- Gradient text (Tailwind) ---
543
- { id: 'gradient-text', regex: /\bbg-clip-text\b/g,
544
- test: (m, line) => /\bbg-gradient-to-/i.test(line),
545
- fmt: () => 'bg-clip-text + bg-gradient' },
546
- // --- Tailwind gray on colored bg ---
547
- { id: 'gray-on-color', regex: /\btext-(?:gray|slate|zinc|neutral|stone)-(\d+)\b/g,
548
- test: (m, line) => /\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/.test(line),
549
- fmt: (m, line) => { const bg = line.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); return `${m[0]} on ${bg?.[0] || '?'}`; } },
550
- // --- Tailwind AI palette ---
551
- { id: 'ai-color-palette', regex: /\btext-(?:purple|violet|indigo)-(\d+)\b/g,
552
- test: (m, line) => /\btext-(?:[2-9]xl|[3-9]xl)\b|<h[1-3]/i.test(line),
553
- fmt: (m) => `${m[0]} on heading` },
554
- { id: 'ai-color-palette', regex: /\bfrom-(?:purple|violet|indigo)-(\d+)\b/g,
555
- test: (m, line) => /\bto-(?:purple|violet|indigo|blue|cyan|pink|fuchsia)-\d+\b/.test(line),
556
- fmt: (m) => `${m[0]} gradient` },
557
- // --- Bounce/elastic easing ---
558
- { id: 'bounce-easing', regex: /\banimate-bounce\b/g,
559
- test: () => true,
560
- fmt: () => 'animate-bounce (Tailwind)' },
561
- { id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
562
- test: () => true,
563
- fmt: (m) => {
564
- const token = m[1]
565
- .split(/[,\s]+/)
566
- .find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
567
- return `animation: ${token || m[1].trim()}`;
568
- } },
569
- { id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
570
- test: (m) => {
571
- const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
572
- return y1 < -0.1 || y1 > 1.1 || y2 < -0.1 || y2 > 1.1;
573
- },
574
- fmt: (m) => `cubic-bezier(${m[1]}, ${m[2]}, ${m[3]}, ${m[4]})` },
575
- // --- Layout property transition ---
576
- // JSX inline style objects use comma-delimited quoted values, not semicolons (issue #548).
577
- { id: 'layout-transition', regex: /transition\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi,
578
- test: (m) => {
579
- const val = (m[2] ?? m[3] ?? '').toLowerCase();
580
- if (/\ball\b/.test(val)) return false;
581
- return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val);
582
- },
583
- fmt: (m) => {
584
- const raw = m[2] ?? m[3] ?? '';
585
- const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
586
- return `transition: ${found ? found.join(', ') : raw.trim()}`;
587
- } },
588
- { id: 'layout-transition', regex: /transition-property\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi,
589
- test: (m) => {
590
- const val = (m[2] ?? m[3] ?? '').toLowerCase();
591
- if (/\ball\b/.test(val)) return false;
592
- return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val);
593
- },
594
- fmt: (m) => {
595
- const raw = m[2] ?? m[3] ?? '';
596
- const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
597
- return `transition-property: ${found ? found.join(', ') : raw.trim()}`;
598
- } },
599
- // --- Broken image: src="" or src="#" or src=" " ---
600
- { id: 'broken-image', regex: /<img\b[^>]*?\bsrc\s*=\s*(?:""|''|"\s+"|'\s+'|"#"|'#')/gi,
601
- test: () => true,
602
- fmt: (m) => m[0].slice(0, 100) },
603
- // --- Broken image: <img> with no src attribute at all ---
604
- { id: 'broken-image', regex: /<img\b(?:(?!\bsrc\s*=)[^>])*>/gi,
605
- test: (m) => !/\bsrc\s*=/i.test(m[0]),
606
- fmt: (m) => m[0].slice(0, 100) },
607
- ];
608
-
609
- const REGEX_ANALYZERS = [
610
- // Flat type hierarchy
611
- (content, filePath) => {
612
- const sizes = new Set();
613
- const REM = 16;
614
- let m;
615
- const sizeRe = /font-size\s*:\s*([\d.]+)(px|rem|em)\b/gi;
616
- while ((m = sizeRe.exec(content)) !== null) {
617
- const px = m[2] === 'px' ? +m[1] : +m[1] * REM;
618
- if (px > 0 && px < 200) sizes.add(Math.round(px * 10) / 10);
619
- }
620
- const clampRe = /font-size\s*:\s*clamp\(\s*([\d.]+)(px|rem|em)\s*,\s*[^,]+,\s*([\d.]+)(px|rem|em)\s*\)/gi;
621
- while ((m = clampRe.exec(content)) !== null) {
622
- sizes.add(Math.round((m[2] === 'px' ? +m[1] : +m[1] * REM) * 10) / 10);
623
- sizes.add(Math.round((m[4] === 'px' ? +m[3] : +m[3] * REM) * 10) / 10);
624
- }
625
- const TW = { 'text-xs': 12, 'text-sm': 14, 'text-base': 16, 'text-lg': 18, 'text-xl': 20, 'text-2xl': 24, 'text-3xl': 30, 'text-4xl': 36, 'text-5xl': 48, 'text-6xl': 60, 'text-7xl': 72, 'text-8xl': 96, 'text-9xl': 128 };
626
- for (const [cls, px] of Object.entries(TW)) { if (new RegExp(`\\b${cls}\\b`).test(content)) sizes.add(px); }
627
- if (sizes.size < 3) return [];
628
- const sorted = [...sizes].sort((a, b) => a - b);
629
- const ratio = sorted[sorted.length - 1] / sorted[0];
630
- if (ratio >= 2.0) return [];
631
- const lines = content.split('\n');
632
- let line = 1;
633
- for (let i = 0; i < lines.length; i++) { if (/font-size/i.test(lines[i]) || /\btext-(?:xs|sm|base|lg|xl|\d)/i.test(lines[i])) { line = i + 1; break; } }
634
- return [finding('flat-type-hierarchy', filePath, `Sizes: ${sorted.map(s => s + 'px').join(', ')} (ratio ${ratio.toFixed(1)}:1)`, line)];
635
- },
636
- // Monotonous spacing (regex)
637
- (content, filePath) => {
638
- const vals = [];
639
- let m;
640
- const pxRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*(\d+)px/gi;
641
- while ((m = pxRe.exec(content)) !== null) { const v = +m[1]; if (v > 0 && v < 200) vals.push(v); }
642
- const remRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*([\d.]+)rem/gi;
643
- while ((m = remRe.exec(content)) !== null) { const v = Math.round(parseFloat(m[1]) * 16); if (v > 0 && v < 200) vals.push(v); }
644
- const gapRe = /gap\s*:\s*(\d+)px/gi;
645
- while ((m = gapRe.exec(content)) !== null) vals.push(+m[1]);
646
- const twRe = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g;
647
- while ((m = twRe.exec(content)) !== null) vals.push(+m[1] * 4);
648
- const rounded = vals.map(v => Math.round(v / 4) * 4);
649
- if (rounded.length < 10) return [];
650
- const counts = {};
651
- for (const v of rounded) counts[v] = (counts[v] || 0) + 1;
652
- const maxCount = Math.max(...Object.values(counts));
653
- const pct = maxCount / rounded.length;
654
- const unique = [...new Set(rounded)].filter(v => v > 0);
655
- if (pct <= 0.6 || unique.length > 3) return [];
656
- const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
657
- return [finding('monotonous-spacing', filePath, `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(pct * 100)}%)`)];
658
- },
659
- // Em-dash overuse (ADVISORY): the AI cadence tell is em-dash *saturation*,
660
- // not the occasional dash. Humans use em-dashes legitimately, so this rule is
661
- // advisory (surfaced separately, never a failure, hook-skipped by default) and
662
- // its threshold is deliberately conservative. Two gates must both hold:
663
- // 1. Absolute floor of EM_DASH_FLOOR (8) dashes — a page with a handful
664
- // never fires, no matter how short.
665
- // 2. Density: at least one dash per EM_DASH_CHARS_PER_DASH (500) characters
666
- // of body text, so a long article that uses eight across several thousand
667
- // words is left alone while a short, dash-per-clause landing page is not.
668
- // Raised from the old flat 5-dash floor, which fired on ordinary long prose.
669
- //
670
- // stripHtmlToText drops tags but leaves character-entity escapes intact, so
671
- // a model that writes `&mdash;`, `&#8212;`, or `&#x2014;` renders an em-dash
672
- // the counter never saw. Decode the em-dash entities (named, zero-padded
673
- // decimal, upper/lower hex) to the literal glyph first. En-dash entities are
674
- // deliberately left alone: the rule counts em-dashes, and the literal `–`
675
- // was never counted either.
676
- (content, filePath) => {
677
- const text = stripHtmlToText(content)
678
- .replace(/&mdash;|&#0*8212;|&#x0*2014;/gi, '—');
679
- let count = 0;
680
- const re = /[—]|--(?=\S)/g;
681
- while (re.exec(text) !== null) count++;
682
- if (count < EM_DASH_FLOOR) return [];
683
- // Saturation gate: dashes must be dense in the prose, not sprinkled through
684
- // a long document. textLength <= count * chars-per-dash means the density is
685
- // at or above the threshold.
686
- if (text.length > count * EM_DASH_CHARS_PER_DASH) return [];
687
- return [finding('em-dash-overuse', filePath, `${count} em-dashes in body text`)];
688
- },
689
- // Marketing buzzwords: SaaS phrase list
690
- (content, filePath) => {
691
- const text = stripHtmlToText(content);
692
- const lower = text.toLowerCase();
693
- const BUZZWORDS = [
694
- 'streamline your', 'empower your', 'supercharge your',
695
- 'unleash your', 'unleash the power', 'leverage the power',
696
- 'built for the modern', 'trusted by leading', 'trusted by the world',
697
- 'best-in-class', 'industry-leading', 'world-class', 'enterprise-grade',
698
- 'next-generation', 'cutting-edge', 'transform your business',
699
- 'revolutionize', 'game-changer', 'game changing',
700
- 'mission-critical', 'best of breed', 'future-proof', 'future proof',
701
- 'seamless experience', 'seamlessly integrate',
702
- 'drive engagement', 'drive growth', 'drive results',
703
- 'harness the power',
704
- ];
705
- let count = 0;
706
- let firstSample = '';
707
- for (const phrase of BUZZWORDS) {
708
- let from = 0;
709
- while (true) {
710
- const idx = lower.indexOf(phrase, from);
711
- if (idx === -1) break;
712
- count++;
713
- if (!firstSample) {
714
- firstSample = text.slice(Math.max(0, idx - 12), Math.min(text.length, idx + phrase.length + 12)).trim();
715
- }
716
- from = idx + phrase.length;
717
- }
718
- }
719
- if (count === 0) return [];
720
- return [finding('marketing-buzzword', filePath, `${count} buzzword phrase${count === 1 ? '' : 's'}: "${firstSample}"`)];
721
- },
722
- // Aphoristic cadence: manufactured-contrast + short-rebuttal
723
- (content, filePath) => {
724
- const text = stripHtmlToText(content);
725
- const NOT_A_RE = /\bNot an? [a-z][^.!?]{1,40}[.!]\s+[A-Z][^.!?]{1,60}[.!]/g;
726
- const SHORT_REBUTTAL_RE = /\b[A-Z][^.!?]{4,80}[.!]\s+(No|Just)\s+[a-z][^.!?]{2,60}[.!]/g;
727
- let count = 0;
728
- let firstSample = '';
729
- let m;
730
- NOT_A_RE.lastIndex = 0;
731
- while ((m = NOT_A_RE.exec(text)) !== null) {
732
- count++;
733
- if (!firstSample) firstSample = m[0].trim().slice(0, 80);
734
- }
735
- SHORT_REBUTTAL_RE.lastIndex = 0;
736
- while ((m = SHORT_REBUTTAL_RE.exec(text)) !== null) {
737
- count++;
738
- if (!firstSample) firstSample = m[0].trim().slice(0, 80);
739
- }
740
- if (count < 3) return [];
741
- return [finding('aphoristic-cadence', filePath, `${count} aphoristic constructions: "${firstSample}"`)];
742
- },
743
- // Dark glow / chromatic halo shadows (page-level). Shared scanner handles
744
- // any color format, single-level var() resolution, zero-offset halos on
745
- // any background, and text-shadow glows.
746
- (content, filePath) => {
747
- const hits = scanCssTextForGlow(content);
748
- if (hits.length === 0) return [];
749
- const lines = content.substring(0, hits[0].index).split('\n');
750
- return [finding('dark-glow', filePath, hits[0].snippet, lines.length)];
751
- },
752
- // Radial-gradient background halo on a dark page (the gradient sibling
753
- // of the dark-glow shadow tell).
754
- (content, filePath) => {
755
- const hits = scanCssTextForRadialHalo(content);
756
- if (hits.length === 0) return [];
757
- const lines = content.substring(0, hits[0].index).split('\n');
758
- return [finding('radial-halo', filePath, hits[0].snippet, lines.length)];
759
- },
760
- // Auto-scrolling marquees (<marquee> or infinite horizontal loop
761
- // animations).
762
- (content, filePath) => scanCssTextForMarquee(content).map(hit => finding('marquee', filePath, hit.snippet)),
763
- ];
764
-
765
- // ---------------------------------------------------------------------------
766
- // Structural CSS checks used by source files whose styles are not parsed by
767
- // the static HTML engine.
768
- // ---------------------------------------------------------------------------
769
-
770
- const CHROMATIC_SHADOW_TOKEN_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
771
-
772
- function insetStripeColorIsChromatic(rawColor) {
773
- const color = String(rawColor || '').trim().replace(/\s*!important\s*$/i, '');
774
- if (/^(?:currentcolor|transparent|inherit|unset)$/i.test(color)) return false;
775
- const variable = color.match(/^var\(\s*(--[\w-]+)/i);
776
- if (variable) return CHROMATIC_SHADOW_TOKEN_RE.test(variable[1]);
777
- if (!/^(?:#|rgba?\(|hsla?\(|hwb\(|oklch\(|oklab\(|lch\(|lab\(|color\(|[a-z]+$)/i.test(color)) return false;
778
- return !isNeutralAuthoredColor(color);
779
- }
780
-
781
- /**
782
- * Blank out comment bodies while preserving every byte offset (and therefore
783
- * every line number) so commented-out CSS is not scanned as live rules.
784
- */
785
- function blankCssComments(css) {
786
- return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '));
787
- }
788
-
789
- function scanInsetStripeCss(rawContent, filePath, lineOffset = 0) {
790
- const content = blankCssComments(rawContent);
791
- const findings = [];
792
- const ruleRe = /([^{};]+)\{([^{}]*)\}/g;
793
- let match;
794
- // Deriving each line with content.slice(0, offset).split('\n') re-scans the
795
- // whole prefix per rule, which is O(n^2) on a large stylesheet. Rule matches
796
- // arrive in source order, so carry a monotonic cursor instead: one pass total.
797
- let scanOffset = 0;
798
- let scanLine = 1;
799
- const lineAtOffset = (offset) => {
800
- while (scanOffset < offset) {
801
- if (content[scanOffset] === '\n') scanLine++;
802
- scanOffset++;
803
- }
804
- return scanLine;
805
- };
806
- while ((match = ruleRe.exec(content)) !== null) {
807
- // The selector group is `[^{};]+`, which greedily absorbs the whitespace and
808
- // newlines trailing the previous rule. Advance past that run before deriving
809
- // the line, or every rule after the first reports the preceding line.
810
- const selectorStart = match.index + (match[1].length - match[1].trimStart().length);
811
- const selector = match[1].trim().replace(/\s+/g, ' ');
812
- if (!selector) continue;
813
- if (/:(?:hover|focus|focus-visible|focus-within|active|checked|target)\b/i.test(selector)) continue;
814
- if (/\[aria-selected\s*[*^$|~]?=\s*["']?true/i.test(selector)) continue;
815
- if (/\[aria-current(?!\s*[*^$|~]?=\s*["']?false)/i.test(selector)) continue;
816
- if (/(?:^|[\s._[-])(?:active|current|selected)(?![\w])/i.test(selector)) continue;
817
- if (/(?:^|[\s>+~,(])(?:button|hr|tr|td|th|table|blockquote|pre|code)(?![\w-])/i.test(selector)) continue;
818
-
819
- // Read the last of a repeated declaration, not the first: that is what the
820
- // cascade paints. Taking the first both flagged stripes that a later
821
- // `box-shadow: none` had cancelled and missed stripes that overrode an
822
- // earlier value, and mis-skipped rules whose narrow width was overridden.
823
- const width = lastMatch(match[2], /(?:^|;)\s*(?:width|inline-size)\s*:\s*(\d+(?:\.\d+)?)px/gi);
824
- if (width && Number(width[1]) <= 40) continue;
825
- const declaration = lastMatch(match[2], /(?:^|;)\s*box-shadow\s*:\s*([^;]+)/gi);
826
- if (!declaration || !/\binset\b/i.test(declaration[1])) continue;
827
- // `!important` qualifies the declaration, not the shadow value, so strip it
828
- // before the layers are read. Tokenizing split it into its own token, which
829
- // made the color count wrong and silently stopped flagging stripes declared
830
- // with it — a shape the previous regex handled.
831
- const shadowValue = declaration[1].replace(/\s*!\s*important\s*$/i, '').trim();
832
-
833
- for (const rawLayer of shadowValue.split(/,(?![^(]*\))/)) {
834
- const layer = rawLayer.trim();
835
- // Parse the layer by its grammar rather than by one spelling of it.
836
- // A box-shadow layer is `inset? && <length>{2,4} && <color>?` in any
837
- // order, so `inset 4px 0 red`, `4px 0 0 red inset`, and `red 4px 0 inset`
838
- // all paint the same stripe. Matching a fixed token order missed three
839
- // valid spellings in a row; enumerate the tokens instead. Tokenizing must
840
- // respect parens: `rgb(0 0 0)` is one color token, and splitting it on
841
- // whitespace would read its channels as lengths.
842
- const tokens = tokenizeShadowLayer(layer);
843
- if (!tokens.some((token) => /^inset$/i.test(token))) continue;
844
- const rest = tokens.filter((token) => !/^inset$/i.test(token));
845
- const lengths = rest.filter(isShadowLength);
846
- const colors = rest.filter((token) => !isShadowLength(token));
847
- // Only the two offsets are required; omitted blur/spread default to 0,
848
- // which is exactly the stripe shape. More than one non-length token is a
849
- // layer shape we do not claim to understand, so leave it alone.
850
- if (lengths.length < 2 || lengths.length > 4 || colors.length !== 1) continue;
851
- const values = lengths.map((token) => ({
852
- n: Number(token.replace(/px$/i, '')),
853
- hasPx: /px$/i.test(token),
854
- }));
855
- const x = values[0];
856
- const y = values[1];
857
- const blur = values[2] ? values[2].n : 0;
858
- const spread = values[3] ? values[3].n : 0;
859
- if ((x.n !== 0 && !x.hasPx) || (y.n !== 0 && !y.hasPx) || blur !== 0 || spread !== 0) continue;
860
- const ax = Math.abs(x.n);
861
- const ay = Math.abs(y.n);
862
- if (!((ax >= 3 && ax <= 12 && ay === 0) || (ay >= 3 && ay <= 12 && ax === 0))) continue;
863
- if (!insetStripeColorIsChromatic(colors[0])) continue;
864
- const edge = ay === 0 ? (x.n > 0 ? 'left' : 'right') : (y.n > 0 ? 'top' : 'bottom');
865
- const line = lineOffset + lineAtOffset(selectorStart);
866
- findings.push(finding('side-tab', filePath, `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`, line));
867
- break;
868
- }
869
- }
870
- return findings;
871
- }
872
-
873
- // ---------------------------------------------------------------------------
874
- // Style block extraction (Astro/Vue/Svelte <style> blocks)
875
- // ---------------------------------------------------------------------------
876
-
877
- function extractStyleBlocks(content, ext) {
878
- ext = ext.toLowerCase();
879
- if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
880
- const blocks = [];
881
- const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
882
- let m;
883
- while ((m = re.exec(content)) !== null) {
884
- const before = content.substring(0, m.index);
885
- const startLine = before.split('\n').length + 1;
886
- blocks.push({ content: m[1], startLine });
887
- }
888
- return blocks;
889
- }
890
-
891
- // ---------------------------------------------------------------------------
892
- // CSS-in-JS extraction (styled-components, emotion)
893
- // ---------------------------------------------------------------------------
894
-
895
- const CSS_IN_JS_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
896
-
897
- function findQuotedStringEnd(content, start, quote) {
898
- for (let cursor = start + 1; cursor < content.length; cursor++) {
899
- if (content[cursor] === '\\') cursor++;
900
- else if (content[cursor] === quote) return cursor;
901
- }
902
- return -1;
903
- }
904
-
905
- function findRegexLiteralEnd(content, start) {
906
- let inCharacterClass = false;
907
- for (let cursor = start + 1; cursor < content.length; cursor++) {
908
- const char = content[cursor];
909
- if (char === '\\') {
910
- cursor++;
911
- } else if (char === '[') {
912
- inCharacterClass = true;
913
- } else if (char === ']') {
914
- inCharacterClass = false;
915
- } else if (char === '/' && !inCharacterClass) {
916
- while (/[A-Za-z]/.test(content[cursor + 1] || '')) cursor++;
917
- return cursor;
918
- } else if (char === '\n' || char === '\r') {
919
- return -1;
920
- }
921
- }
922
- return -1;
923
- }
924
-
925
- function findTemplateExpressionEnd(content, start) {
926
- let depth = 1;
927
- let lastSignificant = '';
928
- let previousSignificant = '';
929
- let antePreviousSignificant = '';
930
- let currentWord = '';
931
- let currentWordPrefix = '';
932
- let wordSeparated = false;
933
- let lastClosedBraceKind = '';
934
- const braceKinds = [];
935
-
936
- const braceKind = () => (
937
- lastSignificant === ')' ||
938
- lastSignificant === ';' ||
939
- lastSignificant === '}' ||
940
- (previousSignificant === '=' && lastSignificant === '>') ||
941
- BLOCK_BRACE_PREFIX_KEYWORDS.has(currentWord)
942
- ? 'block'
943
- : 'expression'
944
- );
945
-
946
- const recordSignificant = (char) => {
947
- if (/\s/.test(char)) {
948
- wordSeparated = true;
949
- return;
950
- }
951
- const isWordChar = /[\w$]/.test(char);
952
- if (isWordChar && (wordSeparated || !currentWord)) {
953
- currentWord = '';
954
- currentWordPrefix = lastSignificant;
955
- } else if (!isWordChar) {
956
- currentWordPrefix = '';
957
- }
958
- wordSeparated = false;
959
- antePreviousSignificant = previousSignificant;
960
- previousSignificant = lastSignificant;
961
- lastSignificant = char;
962
- currentWord = isWordChar ? currentWord + char : '';
963
- };
964
-
965
- for (let cursor = start; cursor < content.length; cursor++) {
966
- const char = content[cursor];
967
- const next = content[cursor + 1];
968
- const afterPostfixUpdate = (lastSignificant === '+' || lastSignificant === '-') &&
969
- previousSignificant === lastSignificant &&
970
- antePreviousSignificant !== lastSignificant;
971
- if (char === "'" || char === '"') {
972
- cursor = findQuotedStringEnd(content, cursor, char);
973
- if (cursor === -1) return -1;
974
- recordSignificant(')');
975
- } else if (char === '/' && next === '/') {
976
- const lineEnd = content.indexOf('\n', cursor + 2);
977
- if (lineEnd === -1) return -1;
978
- cursor = lineEnd;
979
- } else if (char === '/' && next === '*') {
980
- const commentEnd = content.indexOf('*/', cursor + 2);
981
- if (commentEnd === -1) return -1;
982
- cursor = commentEnd + 1;
983
- } else if (
984
- char === '/' &&
985
- (!lastSignificant ||
986
- (/[=([{!?:;,&|+\-*%^~<>]/.test(lastSignificant) && !afterPostfixUpdate) ||
987
- (lastSignificant === '}' && lastClosedBraceKind === 'block') ||
988
- (previousSignificant === '=' && lastSignificant === '>') ||
989
- (currentWordPrefix !== '.' && REGEX_PREFIX_KEYWORDS.has(currentWord)))
990
- ) {
991
- cursor = findRegexLiteralEnd(content, cursor);
992
- if (cursor === -1) return -1;
993
- recordSignificant(')');
994
- } else if (char === '`') {
995
- cursor = findTemplateLiteralEnd(content, cursor);
996
- if (cursor === -1) return -1;
997
- recordSignificant(')');
998
- } else if (char === '{') {
999
- depth++;
1000
- braceKinds.push(braceKind());
1001
- recordSignificant(char);
1002
- } else if (char === '}') {
1003
- depth--;
1004
- if (depth === 0) return cursor;
1005
- lastClosedBraceKind = braceKinds.pop() || '';
1006
- recordSignificant(char);
1007
- } else {
1008
- recordSignificant(char);
1009
- }
1010
- }
1011
- return -1;
1012
- }
1013
-
1014
- function findTemplateLiteralEnd(content, start) {
1015
- for (let cursor = start + 1; cursor < content.length; cursor++) {
1016
- const char = content[cursor];
1017
- if (char === '\\') {
1018
- cursor++;
1019
- } else if (char === '`') {
1020
- return cursor;
1021
- } else if (char === '$' && content[cursor + 1] === '{') {
1022
- cursor = findTemplateExpressionEnd(content, cursor + 2);
1023
- if (cursor === -1) return -1;
1024
- }
1025
- }
1026
- return -1;
1027
- }
1028
-
1029
- function findCSSinJSTemplates(content) {
1030
- const templates = [];
1031
- const tagRe = /\b(?:styled(?:\.\w+|\([^)]+\))|css)/g;
1032
- let match;
1033
- while ((match = tagRe.exec(content)) !== null) {
1034
- let cursor = match.index + match[0].length;
1035
- while (/\s/.test(content[cursor] || '')) cursor++;
1036
-
1037
- if (content[cursor] === '<') {
1038
- let depth = 0;
1039
- while (cursor < content.length) {
1040
- const char = content[cursor];
1041
- if (char === '<') depth++;
1042
- else if (char === '>' && content[cursor - 1] !== '=') depth--;
1043
- cursor++;
1044
- if (depth === 0) break;
1045
- }
1046
- if (depth !== 0) continue;
1047
- while (/\s/.test(content[cursor] || '')) cursor++;
1048
- }
1049
-
1050
- if (content[cursor] !== '`') continue;
1051
- const contentStart = cursor + 1;
1052
- cursor = findTemplateLiteralEnd(content, cursor);
1053
- if (cursor === -1) continue;
1054
-
1055
- templates.push({
1056
- tagStart: match.index,
1057
- contentStart,
1058
- contentEnd: cursor,
1059
- });
1060
- tagRe.lastIndex = cursor + 1;
1061
- }
1062
- return templates;
1063
- }
1064
-
1065
- function extractCSSinJS(content, ext) {
1066
- ext = ext.toLowerCase();
1067
- if (!CSS_IN_JS_EXTENSIONS.has(ext)) return [];
1068
- return findCSSinJSTemplates(content).map((template) => {
1069
- const before = content.substring(0, template.tagStart);
1070
- const startLine = before.split('\n').length;
1071
- return {
1072
- content: content.slice(template.contentStart, template.contentEnd),
1073
- startLine,
1074
- };
1075
- });
1076
- }
1077
-
1078
- function stripCssInJsComments(content, ext) {
1079
- if (!CSS_IN_JS_EXTENSIONS.has(ext.toLowerCase())) return content;
1080
- const templates = findCSSinJSTemplates(content);
1081
- let output = '';
1082
- let cursor = 0;
1083
- for (const template of templates) {
1084
- output += content.slice(cursor, template.contentStart);
1085
- output += stripCssComments(content.slice(template.contentStart, template.contentEnd));
1086
- cursor = template.contentEnd;
1087
- }
1088
- return output + content.slice(cursor);
1089
- }
1090
-
1091
- function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null, options = {}) {
1092
- const { profile, phase = 'regex-matchers' } = options || {};
1093
- const findings = [];
1094
- if (!profile) {
1095
- for (const matcher of REGEX_MATCHERS) {
1096
- for (let i = 0; i < lines.length; i++) {
1097
- const line = lines[i];
1098
- matcher.regex.lastIndex = 0;
1099
- let m;
1100
- while ((m = matcher.regex.exec(line)) !== null) {
1101
- // For extracted blocks, use nearby lines as context for multi-line CSS patterns
1102
- const context = blockContext
1103
- ? lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join(' ')
1104
- : line;
1105
- if (matcher.test(m, context)) {
1106
- findings.push(finding(matcher.id, filePath, matcher.fmt(m, context), i + 1 + lineOffset));
1107
- }
1108
- }
1109
- }
1110
- }
1111
- return findings;
1112
- }
1113
-
1114
- for (const matcher of REGEX_MATCHERS) {
1115
- const matcherFindings = profileFindings(profile, {
1116
- engine: 'regex',
1117
- phase,
1118
- ruleId: matcher.id,
1119
- target: filePath,
1120
- }, () => {
1121
- const matches = [];
1122
- for (let i = 0; i < lines.length; i++) {
1123
- const line = lines[i];
1124
- matcher.regex.lastIndex = 0;
1125
- let m;
1126
- while ((m = matcher.regex.exec(line)) !== null) {
1127
- // For extracted blocks, use nearby lines as context for multi-line CSS patterns
1128
- const context = blockContext
1129
- ? lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join(' ')
1130
- : line;
1131
- if (matcher.test(m, context)) {
1132
- matches.push(finding(matcher.id, filePath, matcher.fmt(m, context), i + 1 + lineOffset));
1133
- }
1134
- }
1135
- }
1136
- return matches;
1137
- });
1138
- findings.push(...matcherFindings);
1139
- }
1140
- return findings;
1141
- }
1142
-
1143
- /** Page-level analyzers that scan rendered text content (em-dash use,
1144
- * buzzword phrases, aphoristic cadence).
1145
- * These are detector-agnostic — they work on any HTML/text source
1146
- * and don't need a parsed DOM. Exported so detectHtml can call them
1147
- * for `.html` files (which otherwise skip the regex engine). */
1148
- const TEXT_CONTENT_ANALYZER_IDS = [
1149
- 'em-dash-overuse',
1150
- 'marketing-buzzword',
1151
- 'aphoristic-cadence',
1152
- ];
1153
-
1154
- function runTextContentAnalyzers(content, filePath, options = {}) {
1155
- const profile = options?.profile;
1156
- if (!shouldRunPageAnalyzers(content, filePath)) return [];
1157
- // The 3 text-content analyzers are at indices 2-4 in REGEX_ANALYZERS
1158
- // (single-font's removal on 2026-07-29 shifted every index down one).
1159
- const findings = [];
1160
- for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
1161
- const analyzer = REGEX_ANALYZERS[2 + i];
1162
- const ruleId = TEXT_CONTENT_ANALYZER_IDS[i];
1163
- findings.push(...profileFindings(profile, {
1164
- engine: 'regex',
1165
- phase: 'text-content',
1166
- ruleId,
1167
- target: filePath,
1168
- }, () => analyzer(content, filePath)));
1169
- }
1170
- return findings;
1171
- }
1172
-
1173
- function detectText(content, filePath, options = {}) {
1174
- const profile = options?.profile;
1175
- const findings = [];
1176
- const ext = extFromFilePath(filePath);
1177
- const commentStrippedSource = JS_SOURCE_EXTS.has(ext) ? stripJsComments(content, {
1178
- jsx: ext === '.js' || ext === '.jsx' || ext === '.tsx',
1179
- }) : blankCommentsForMatchers(content, ext);
1180
- const source = stripCssInJsComments(commentStrippedSource, ext);
1181
- const lines = source.split('\n');
1182
-
1183
- // Run regex matchers on the full file content (catches Tailwind classes, inline styles)
1184
- // Enable block context for CSS files where related properties span multiple lines
1185
- findings.push(...runRegexMatchers(lines, filePath, 0, STYLESHEET_EXTS.has(ext) || null, {
1186
- profile,
1187
- phase: 'source',
1188
- }));
1189
- // Pseudo-element stripes (::before/::after absolute bars) carry the same
1190
- // side-tab silhouette without any border token, so the line matchers can't
1191
- // see them (issue #394). The shared scanner already runs on full HTML pages
1192
- // via checkHtmlPatterns; give standalone stylesheets, component style
1193
- // blocks, and CSS-in-JS templates the same coverage. Each hit carries the
1194
- // rule's source offset, so the finding gets a real line and line-scoped
1195
- // inline ignores keep working.
1196
- const pseudoStripeFindings = (text, lineOffset) =>
1197
- scanCssTextForPseudoStripe(text).map(hit =>
1198
- finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
1199
-
1200
- if (STYLESHEET_EXTS.has(ext)) {
1201
- findings.push(...scanInsetStripeCss(content, filePath));
1202
- findings.push(...pseudoStripeFindings(content, 0));
1203
- }
1204
-
1205
- // Block-level CSS checks that need multiple declarations must run over the
1206
- // complete source, not line-by-line. This covers standalone stylesheets,
1207
- // component style blocks, inline styles, and CSS-in-JS templates.
1208
- findings.push(...profileFindings(profile, {
1209
- engine: 'regex',
1210
- phase: 'source',
1211
- ruleId: 'codex-grid-background',
1212
- target: filePath,
1213
- }, () => scanCssTextForGridBackground(source).map(hit => {
1214
- const line = source.substring(0, hit.index).split('\n').length;
1215
- return finding('codex-grid-background', filePath, hit.snippet, line);
1216
- })));
1217
-
1218
- // Extract and scan <style> blocks from Astro/Vue/Svelte components.
1219
- const styleBlocks = profile
1220
- ? profileStep(profile, {
1221
- engine: 'regex',
1222
- phase: 'extract',
1223
- ruleId: 'style-blocks',
1224
- target: filePath,
1225
- }, () => extractStyleBlocks(content, ext))
1226
- : extractStyleBlocks(content, ext);
1227
- for (const block of styleBlocks) {
1228
- const blockContent = blankCssLineComments(stripCssComments(block.content));
1229
- const blockLines = blockContent.split('\n');
1230
- findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
1231
- profile,
1232
- phase: 'style-block',
1233
- }));
1234
- // block.startLine is the first line *after* the <style> tag, but block.content
1235
- // begins at the character right after that tag — so its own line 1 sits on the
1236
- // tag's line, whether or not a newline follows immediately. lineAtOffset is
1237
- // 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
1238
- // reported every selector one line low. runRegexMatchers keeps startLine - 1
1239
- // because it indexes its split lines from zero.
1240
- findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 2));
1241
- findings.push(...pseudoStripeFindings(blockContent, block.startLine - 2));
1242
- }
1243
-
1244
- // Extract and scan CSS-in-JS template literals
1245
- const cssJsBlocks = profile
1246
- ? profileStep(profile, {
1247
- engine: 'regex',
1248
- phase: 'extract',
1249
- ruleId: 'css-in-js',
1250
- target: filePath,
1251
- }, () => extractCSSinJS(source, ext))
1252
- : extractCSSinJS(source, ext);
1253
- for (const block of cssJsBlocks) {
1254
- const blockContent = stripCssComments(block.content);
1255
- const blockLines = blockContent.split('\n');
1256
- findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
1257
- profile,
1258
- phase: 'css-in-js',
1259
- }));
1260
- findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 1));
1261
- findings.push(...pseudoStripeFindings(blockContent, block.startLine - 1));
1262
- }
1263
-
1264
- if (options?.designSystem) {
1265
- findings.push(...profileFindings(profile, {
1266
- engine: 'regex',
1267
- phase: 'source',
1268
- ruleId: 'design-system',
1269
- target: filePath,
1270
- }, () => checkSourceDesignSystem(content, filePath, { designSystem: options.designSystem })));
1271
- }
1272
-
1273
- // Deduplicate findings (same antipattern + similar snippet, within 2 lines)
1274
- const deduped = [];
1275
- for (const f of findings) {
1276
- const isDupe = deduped.some(d =>
1277
- d.antipattern === f.antipattern &&
1278
- d.snippet === f.snippet &&
1279
- Math.abs(d.line - f.line) <= 2
1280
- );
1281
- if (!isDupe) deduped.push(f);
1282
- }
1283
-
1284
- // Page-level analyzers only run on full pages
1285
- if (shouldRunPageAnalyzers(content, filePath)) {
1286
- const analyzerIds = [
1287
- 'flat-type-hierarchy',
1288
- 'monotonous-spacing',
1289
- 'em-dash-overuse',
1290
- 'marketing-buzzword',
1291
- 'aphoristic-cadence',
1292
- 'dark-glow',
1293
- ];
1294
- for (let i = 0; i < REGEX_ANALYZERS.length; i++) {
1295
- const analyzer = REGEX_ANALYZERS[i];
1296
- deduped.push(...profileFindings(profile, {
1297
- engine: 'regex',
1298
- phase: 'page-analyzer',
1299
- ruleId: analyzerIds[i] || `analyzer-${i + 1}`,
1300
- target: filePath,
1301
- }, () => analyzer(content, filePath)));
1302
- }
1303
- }
1304
-
1305
- // Inline `impeccable-disable*` waivers travel with the file; honor them unless
1306
- // explicitly bypassed (`--no-config` / `--no-inline-ignores`).
1307
- return options?.inlineIgnores === false ? deduped : applyInlineIgnores(deduped, content);
1308
- }
1309
-
1310
- export {
1311
- REGEX_MATCHERS,
1312
- REGEX_ANALYZERS,
1313
- TEXT_CONTENT_ANALYZER_IDS,
1314
- extractStyleBlocks,
1315
- extractCSSinJS,
1316
- runRegexMatchers,
1317
- runTextContentAnalyzers,
1318
- detectText,
1319
- };