quiver-cli 0.8.0 → 1.1.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 (200) hide show
  1. package/README.md +127 -45
  2. package/bin/quiver-cli.mjs +3 -1
  3. package/dist/cli.js +666 -436
  4. package/package.json +2 -2
  5. package/template/.agents/AGENTS.md +4 -3
  6. package/template/.agents/config.json +7 -0
  7. package/template/.agents/plugins/opencode/rtk.ts +34 -0
  8. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  9. package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
  10. package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
  11. package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
  12. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
  13. package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
  14. package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
  15. package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
  16. package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
  17. package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
  18. package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
  19. package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
  21. package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
  22. package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
  23. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
  24. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
  25. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
  26. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  27. package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
  28. package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
  29. package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
  30. package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
  31. package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
  32. package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
  33. package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
  34. package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
  35. package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
  36. package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
  37. package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
  38. package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
  39. package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
  40. package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
  41. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
  42. package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
  43. package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
  44. package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
  45. package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
  46. package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
  47. package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
  48. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
  49. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
  50. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
  51. package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
  52. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
  53. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  54. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
  55. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
  56. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
  57. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  58. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
  59. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
  60. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
  61. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
  62. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
  63. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
  64. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
  65. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
  66. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  67. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
  68. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
  69. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
  70. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
  71. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
  72. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
  73. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
  74. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  76. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
  78. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
  79. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  85. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
  86. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
  88. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
  89. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
  90. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
  91. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
  92. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
  95. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  96. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  97. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  98. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  99. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  100. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  101. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  102. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  103. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  104. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
  105. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
  106. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
  107. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
  108. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
  109. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
  110. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
  111. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
  112. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
  113. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
  114. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  115. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
  116. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
  117. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
  118. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  119. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
  120. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
  121. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
  122. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  123. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
  124. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
  125. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
  126. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
  127. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
  128. package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
  129. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
  130. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
  131. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
  132. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
  133. package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
  134. package/template/.agents/skills/design/shadcn/cli.md +49 -16
  135. package/template/.agents/skills/design/shadcn/customization.md +14 -7
  136. package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
  137. package/template/.agents/skills/design/shadcn/mcp.md +27 -16
  138. package/template/.agents/skills/design/shadcn/registry.md +277 -0
  139. package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
  140. package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
  141. package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
  142. package/template/.agents/skills/find-skills/SKILL.md +2 -3
  143. package/template/.agents/skills/hono/SKILL.md +579 -0
  144. package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
  145. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
  146. package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
  147. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
  148. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
  149. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
  150. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
  151. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
  152. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
  153. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
  154. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
  155. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
  156. package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
  157. package/template/.agents/skills/supabase/SKILL.md +145 -0
  158. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
  159. package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
  160. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  161. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  162. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  163. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  164. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  165. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  166. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  167. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  168. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  169. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  170. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  171. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  172. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  173. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  174. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  175. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  176. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  177. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  178. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  179. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  180. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  181. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  182. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  183. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  184. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  185. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  186. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  187. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  188. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  189. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  190. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  191. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  192. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  193. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  194. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  195. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  196. package/template/.agents/upstreams.json +34 -12
  197. package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
  198. package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
  199. package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
  200. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs';
5
- import { parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs';
5
+ import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs';
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // jsdom CSS-variable border override map
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
223
223
  // ---------------------------------------------------------------------------
224
224
 
225
225
  const STATIC_INHERITED_PROPS = new Set([
226
- 'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
226
+ 'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
227
227
  'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
228
228
  'webkitHyphens',
229
229
  ]);
@@ -245,9 +245,14 @@ const STATIC_DEFAULT_STYLE = {
245
245
  outlineColor: 'rgb(0, 0, 0)',
246
246
  outlineStyle: 'none',
247
247
  boxShadow: 'none',
248
+ // NOT in STATIC_INHERITED_PROPS even though text-shadow inherits in real
249
+ // CSS: the glow check only needs to fire once, on the element that
250
+ // declares the shadow, not on every descendant.
251
+ textShadow: 'none',
248
252
  fontFamily: '',
249
253
  fontSize: '16px',
250
254
  fontStyle: 'normal',
255
+ fontVariant: 'normal',
251
256
  fontWeight: '400',
252
257
  lineHeight: 'normal',
253
258
  letterSpacing: 'normal',
@@ -302,6 +307,7 @@ const STATIC_PROP_MAP = {
302
307
  'outline-color': 'outlineColor',
303
308
  'outline-style': 'outlineStyle',
304
309
  'box-shadow': 'boxShadow',
310
+ 'text-shadow': 'textShadow',
305
311
  'font-family': 'fontFamily',
306
312
  'font-size': 'fontSize',
307
313
  'font-style': 'fontStyle',
@@ -339,18 +345,29 @@ const STATIC_PROP_MAP = {
339
345
  'overflow-y': 'overflowY',
340
346
  };
341
347
 
348
+ // parseStaticColor tries parseAnyColor first, which already resolves every
349
+ // name in the shared CSS_NAMED_COLORS table. This fallback only carries the
350
+ // keywords parseAnyColor deliberately returns null for: the cascade needs
351
+ // `transparent` to read as an actual zero-alpha color.
342
352
  const STATIC_NAMED_COLORS = {
343
- black: { r: 0, g: 0, b: 0, a: 1 },
344
- white: { r: 255, g: 255, b: 255, a: 1 },
345
353
  transparent: { r: 0, g: 0, b: 0, a: 0 },
346
- gray: { r: 128, g: 128, b: 128, a: 1 },
347
- grey: { r: 128, g: 128, b: 128, a: 1 },
348
- silver: { r: 192, g: 192, b: 192, a: 1 },
349
- red: { r: 255, g: 0, b: 0, a: 1 },
350
- green: { r: 0, g: 128, b: 0, a: 1 },
351
- blue: { r: 0, g: 0, b: 255, a: 1 },
352
354
  };
353
355
 
356
+ // Named-color alternation for plucking a color token out of shorthand values
357
+ // (issue #359: a hardcoded 9-name list here silently dropped `purple`,
358
+ // `crimson`, `teal`, ... from border shorthands, so the side defaulted to
359
+ // neutral black and side-tab never fired on .html files). Derived from the
360
+ // same table parseAnyColor resolves against, so extraction and parsing can't
361
+ // drift apart. Longest-first so names containing other names as substrings
362
+ // (rebeccapurple) are matched whole.
363
+ const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)]
364
+ .sort((a, b) => b.length - a.length)
365
+ .join('|');
366
+ const STATIC_COLOR_TOKEN_RE = new RegExp(
367
+ `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`,
368
+ 'i'
369
+ );
370
+
354
371
  function splitCssList(value) {
355
372
  const parts = [];
356
373
  let depth = 0, quote = '', start = 0;
@@ -420,7 +437,23 @@ function extractStaticColor(value) {
420
437
  if (!value) return '';
421
438
  const raw = String(value).trim();
422
439
  if (/^var\(/i.test(raw)) return raw;
423
- const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i);
440
+ // color-mix(...) needs balanced-paren capture (its arguments regularly
441
+ // contain nested var()/oklch() calls AND the keyword `transparent`, which
442
+ // the flat regex below would otherwise pluck out of the middle of the
443
+ // expression and report as the whole color).
444
+ const mixStart = raw.search(/color-mix\(/i);
445
+ if (mixStart !== -1) {
446
+ let depth = 0;
447
+ for (let i = raw.indexOf('(', mixStart); i < raw.length; i++) {
448
+ if (raw[i] === '(') depth++;
449
+ else if (raw[i] === ')') {
450
+ depth--;
451
+ if (depth === 0) return raw.slice(mixStart, i + 1);
452
+ }
453
+ }
454
+ return '';
455
+ }
456
+ const colorLike = raw.match(STATIC_COLOR_TOKEN_RE);
424
457
  if (!colorLike) return '';
425
458
  return colorLike[0];
426
459
  }
@@ -532,6 +565,15 @@ function expandStaticDeclaration(prop, value) {
532
565
  const beforeImage = hasImage ? v.split(/(?:repeating-)?(?:linear|radial|conic)-gradient\(|url\(/i)[0] : v;
533
566
  const color = extractStaticColor(hasImage ? beforeImage : v);
534
567
  if (color) out.push(['backgroundColor', color]);
568
+ // The `background` shorthand resets every longhand it does not set.
569
+ // Without this, `pre code { background: none }` leaves an earlier
570
+ // `background: var(--surface)` color standing and the contrast checks
571
+ // measure text against a surface the browser never paints. var() values
572
+ // stay untouched: they may resolve to a color later in the pipeline.
573
+ if (!color && !hasImage && !/var\(/i.test(v)) {
574
+ out.push(['backgroundColor', 'rgba(0, 0, 0, 0)']);
575
+ out.push(['backgroundImage', 'none']);
576
+ }
535
577
  return out;
536
578
  }
537
579
  if (p === 'border') {
@@ -702,7 +744,20 @@ function collectStaticCssRules(cssText, csstree) {
702
744
  });
703
745
  });
704
746
  for (const selector of splitCssList(selectorText)) {
705
- if (selector) rules.push({ selector, declarations, specificity: staticSpecificity(selector), order: order++ });
747
+ if (!selector) continue;
748
+ // :hover rules can't be matched statically as-is (no interaction
749
+ // state), but they carry real cascade weight while hovered. Tag
750
+ // them and record a state-stripped selector so the hover pass can
751
+ // find their targets; specificity stays computed from the ORIGINAL
752
+ // selector (per CSS, :hover counts as a class).
753
+ const isHover = /:hover\b/i.test(selector);
754
+ let matchSelector = null;
755
+ if (isHover) {
756
+ matchSelector = selector.replace(/:hover\b/gi, '').trim();
757
+ if (!matchSelector || /[>+~]\s*$/.test(matchSelector)) matchSelector = null;
758
+ else matchSelector = matchSelector.replace(/(^|[\s>+~])(?=$|[\s>+~])/g, '$1*');
759
+ }
760
+ rules.push({ selector, declarations, specificity: staticSpecificity(selector), order: order++, isHover, matchSelector });
706
761
  }
707
762
  return;
708
763
  }
@@ -805,6 +860,13 @@ class StaticDocument {
805
860
  this.domutils = modules.domutils;
806
861
  this._wrappers = new WeakMap();
807
862
  this._styleMap = new WeakMap();
863
+ this._hoverStyleMap = new WeakMap();
864
+ this._accentDashPseudo = new WeakSet();
865
+ // Elements whose ::before/::after paints a full-cover opaque surface
866
+ // (position absolute/fixed + inset 0 + solid background). The pseudo is
867
+ // the element's visible background for contrast purposes even though it
868
+ // never joins the element cascade.
869
+ this._pseudoSurface = new WeakMap();
808
870
  }
809
871
  wrap(node) {
810
872
  let wrapped = this._wrappers.get(node);
@@ -841,6 +903,24 @@ class StaticDocument {
841
903
  getStyle(el) {
842
904
  return this._styleMap.get(el.node) || makeStaticStyle();
843
905
  }
906
+ setHoverStyle(node, style) {
907
+ this._hoverStyleMap.set(node, style);
908
+ }
909
+ getHoverStyle(el) {
910
+ return this._hoverStyleMap.get(el.node) || null;
911
+ }
912
+ setAccentDashPseudo(node) {
913
+ this._accentDashPseudo.add(node);
914
+ }
915
+ hasAccentDashPseudo(el) {
916
+ return this._accentDashPseudo.has(el.node);
917
+ }
918
+ setPseudoSurface(node, color) {
919
+ this._pseudoSurface.set(node, color);
920
+ }
921
+ getPseudoSurface(el) {
922
+ return this._pseudoSurface.get(el.node) || null;
923
+ }
844
924
  }
845
925
 
846
926
  function makeStaticStyle(values = {}) {
@@ -856,6 +936,9 @@ function buildStaticWindow(staticDoc) {
856
936
  return {
857
937
  document: staticDoc,
858
938
  getComputedStyle: (el) => staticDoc.getStyle(el),
939
+ getHoverStyle: (el) => staticDoc.getHoverStyle(el),
940
+ hasAccentDashPseudo: (el) => staticDoc.hasAccentDashPseudo(el),
941
+ getPseudoSurface: (el) => staticDoc.getPseudoSurface(el),
859
942
  };
860
943
  }
861
944
 
@@ -869,7 +952,10 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
869
952
  const rel = link.attribs?.rel || '';
870
953
  const href = link.attribs?.href || '';
871
954
  if (!/\bstylesheet\b/i.test(rel) || !href || /^(https?:)?\/\//i.test(href)) continue;
872
- const cssPath = path.resolve(fileDir, href);
955
+ // Cache-busting hrefs (styles.css?v=3) resolve to the file, not to a
956
+ // literal path with the query in it; a versioned link otherwise made the
957
+ // whole stylesheet invisible to every element-level check.
958
+ const cssPath = path.resolve(fileDir, href.split(/[?#]/)[0]);
873
959
  try {
874
960
  const css = profileStep(profile, {
875
961
  engine: 'static-html',
@@ -886,6 +972,13 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
886
972
 
887
973
  function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePath) {
888
974
  const specified = new Map();
975
+ // Declarations from :hover rules, matched via their state-stripped
976
+ // selectors. Merged per-property against the resting cascade in
977
+ // computeNode — a hover declaration only takes effect if it would win
978
+ // the cascade while the element is hovered (all resting rules still
979
+ // apply in that state).
980
+ const hoverSpecified = new Map();
981
+ const rootCustomProps = collectCssCustomProps(cssText);
889
982
  const allNodes = modules.selectAll('*', root.children || []);
890
983
  const rules = profileStep(profile, {
891
984
  engine: 'static-html',
@@ -901,9 +994,65 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
901
994
  target: filePath,
902
995
  }, () => {
903
996
  for (const rule of rules) {
997
+ // ::before/::after rules can't join the element cascade (pseudo
998
+ // elements aren't DOM nodes), but one shape matters to the eyebrow
999
+ // check: the short chromatic "kicker dash" (content box 8-80px wide,
1000
+ // 1-6px tall, accent-colored fill). Mark the base-selector matches
1001
+ // so checkElementHeroEyebrow can see the dash.
1002
+ if (!rule.isHover) {
1003
+ const pm = rule.selector.match(/^(.+?)\s*::?(?:before|after)$/i);
1004
+ if (pm) {
1005
+ const decls = new Map();
1006
+ for (const d of rule.declarations) decls.set(d.prop.toLowerCase(), d.value);
1007
+ const w = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', rootCustomProps));
1008
+ const h = cssLengthToPx(resolveVarRefs(decls.get('height') || decls.get('block-size') || '', rootCustomProps));
1009
+ if (w != null && h != null && w >= 8 && w <= 80 && h >= 1 && h <= 6) {
1010
+ const bgRaw = String(resolveVarRefs(decls.get('background-color') || decls.get('background') || '', rootCustomProps));
1011
+ const token = bgRaw.match(/(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b/i);
1012
+ const c = parseAnyColor(token ? token[0] : bgRaw);
1013
+ if (c && (c.a ?? 1) >= 0.1 && Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) >= 30) {
1014
+ try {
1015
+ for (const node of modules.selectAll(pm[1], root.children || [])) {
1016
+ staticDoc.setAccentDashPseudo(node);
1017
+ }
1018
+ } catch { /* unsupported base selector */ }
1019
+ }
1020
+ }
1021
+ // Full-cover surface pseudo: the CTA construction where the
1022
+ // element itself stays transparent and a ::before/::after with
1023
+ // position absolute/fixed + inset 0 (or all four sides 0, or
1024
+ // 100% width and height) plus an opaque background paints the
1025
+ // visible surface. Mark base-selector matches so the contrast
1026
+ // checks measure text against the surface the browser renders.
1027
+ const pseudoPos = String(decls.get('position') || '').toLowerCase();
1028
+ if (pseudoPos === 'absolute' || pseudoPos === 'fixed') {
1029
+ const zeroLen = v => v != null && /^0(?:px)?$/.test(String(v).trim());
1030
+ const insetRaw = String(decls.get('inset') || '').trim();
1031
+ const coversBox = (insetRaw !== '' && insetRaw.split(/\s+/).every(t => /^0(?:px)?$/.test(t)))
1032
+ || ['top', 'right', 'bottom', 'left'].every(side => zeroLen(decls.get(side)))
1033
+ || (String(decls.get('width') || '').trim() === '100%'
1034
+ && String(decls.get('height') || '').trim() === '100%');
1035
+ if (coversBox && decls.has('content')) {
1036
+ const surfRaw = String(resolveVarRefs(decls.get('background-color') || decls.get('background') || '', rootCustomProps));
1037
+ const surfToken = surfRaw.match(/(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b/i);
1038
+ const surf = parseAnyColor(surfToken ? surfToken[0] : surfRaw);
1039
+ if (surf && (surf.a ?? 1) >= 0.9 && !/gradient/i.test(surfRaw)) {
1040
+ try {
1041
+ for (const node of modules.selectAll(pm[1], root.children || [])) {
1042
+ staticDoc.setPseudoSurface(node, surf);
1043
+ }
1044
+ } catch { /* unsupported base selector */ }
1045
+ }
1046
+ }
1047
+ }
1048
+ continue;
1049
+ }
1050
+ }
1051
+ const matchSelector = rule.isHover ? rule.matchSelector : rule.selector;
1052
+ if (!matchSelector) continue;
904
1053
  let matched;
905
1054
  try {
906
- matched = modules.selectAll(rule.selector, root.children || []);
1055
+ matched = modules.selectAll(matchSelector, root.children || []);
907
1056
  } catch {
908
1057
  recordProfileEvent(profile, {
909
1058
  engine: 'static-html',
@@ -912,13 +1061,13 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
912
1061
  target: filePath,
913
1062
  ms: 0,
914
1063
  findings: 0,
915
- detail: rule.selector,
1064
+ detail: matchSelector,
916
1065
  });
917
1066
  continue;
918
1067
  }
919
1068
  for (const node of matched) {
920
1069
  for (const decl of rule.declarations) {
921
- applyStaticDeclaration(specified, node, decl.prop, decl.value, {
1070
+ applyStaticDeclaration(rule.isHover ? hoverSpecified : specified, node, decl.prop, decl.value, {
922
1071
  important: decl.important,
923
1072
  specificity: rule.specificity,
924
1073
  order: rule.order,
@@ -961,6 +1110,28 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
961
1110
  }
962
1111
  const style = makeStaticStyle(values);
963
1112
  staticDoc.setStyle(node, style);
1113
+
1114
+ // Hover pass: limited to the two properties the hover-contrast check
1115
+ // consumes. A hover declaration wins only if it beats the resting
1116
+ // winner for that property under normal cascade rules (specificity /
1117
+ // order / importance) — exactly what a browser computes while the
1118
+ // element is hovered.
1119
+ const hoverMap = hoverSpecified.get(node);
1120
+ if (hoverMap) {
1121
+ let hoverValues = null;
1122
+ for (const prop of ['color', 'backgroundColor']) {
1123
+ const hoverDecl = hoverMap.get(prop);
1124
+ if (!hoverDecl) continue;
1125
+ const restingDecl = specifiedMap.get(prop);
1126
+ if (!compareStaticPriority(restingDecl, hoverDecl)) continue;
1127
+ const next = normalizeStaticCssValue(prop, hoverDecl.value, customProps, parentStyle, values);
1128
+ if (next === values[prop]) continue;
1129
+ if (!hoverValues) hoverValues = { ...values };
1130
+ hoverValues[prop] = next;
1131
+ }
1132
+ if (hoverValues) staticDoc.setHoverStyle(node, makeStaticStyle(hoverValues));
1133
+ }
1134
+
964
1135
  for (const child of node.children || []) {
965
1136
  if (child.type === 'tag') computeNode(child, style, customProps);
966
1137
  }
@@ -18,20 +18,23 @@ import {
18
18
  checkElementGlow,
19
19
  checkElementGptBorderShadow,
20
20
  checkElementHeroEyebrow,
21
+ checkElementHoverContrast,
21
22
  checkElementIconTile,
22
23
  checkElementItalicSerif,
23
24
  checkElementMotion,
24
25
  checkElementOversizedH1,
25
26
  checkElementQuality,
27
+ checkElementRadialSpotlight,
26
28
  checkCreamPalette,
27
29
  checkHtmlPatterns,
30
+ checkKickerAboveHeadingFromDoc,
31
+ checkNumberedSectionLabelsFromDoc,
28
32
  checkPageLayout,
29
33
  checkPageQualityFromDoc,
30
- checkRepeatedSectionKickersFromDoc,
34
+ checkRepeatedContainerTextFromDoc,
31
35
  resolveBackground,
32
36
  resolveBorderRadiusPx,
33
37
  } from '../../rules/checks.mjs';
34
- import { filterByProviders } from '../../registry/antipatterns.mjs';
35
38
  import { detectText, runTextContentAnalyzers } from '../regex/detect-text.mjs';
36
39
  import {
37
40
  StaticDocument,
@@ -57,9 +60,6 @@ function checkStaticPageTypography(document, window) {
57
60
  for (const font of overusedFound) {
58
61
  findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` });
59
62
  }
60
- if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) {
61
- findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` });
62
- }
63
63
  const sizes = new Set();
64
64
  for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div')) {
65
65
  const fontSize = parseFloat(window.getComputedStyle(el).fontSize);
@@ -90,8 +90,9 @@ function checkElementBrokenImage(el) {
90
90
  }
91
91
 
92
92
  const STATIC_ELEMENT_RULES = [
93
- { id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window)) },
93
+ { id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window), el) },
94
94
  { id: 'color-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementColors(el, style, tag, window, customPropMap, false) },
95
+ { id: 'hover-color-rules', selector: '*', run: (el, tag, style, window) => checkElementHoverContrast(el, style, tag, window) },
95
96
  { id: 'dark-glow', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementGlow(tag, style, resolveBackground(el.parentElement || el, window, customPropMap)) },
96
97
  { id: 'motion-rules', selector: '*', run: (el, tag, style) => checkElementMotion(tag, style) },
97
98
  { id: 'icon-tile-stack', selector: 'h1,h2,h3,h4,h5,h6', run: (el, tag, _style, window) => checkElementIconTile(el, tag, window) },
@@ -102,6 +103,7 @@ const STATIC_ELEMENT_RULES = [
102
103
  { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) },
103
104
  { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) },
104
105
  { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) },
106
+ { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) },
105
107
  ];
106
108
 
107
109
  async function detectHtml(filePath, options = {}) {
@@ -197,7 +199,13 @@ async function detectHtml(filePath, options = {}) {
197
199
  for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
198
200
  findings.push(finding(f.id, filePath, f.snippet));
199
201
  }
200
- for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
202
+ for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
203
+ findings.push(finding(f.id, filePath, f.snippet));
204
+ }
205
+ for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
206
+ findings.push(finding(f.id, filePath, f.snippet));
207
+ }
208
+ for (const f of runPageCheck('repeated-container-text', () => checkRepeatedContainerTextFromDoc(document, window))) {
201
209
  findings.push(finding(f.id, filePath, f.snippet));
202
210
  }
203
211
  for (const f of runPageCheck('layout-rules', () => checkPageLayout(document, window))) {
@@ -209,10 +217,33 @@ async function detectHtml(filePath, options = {}) {
209
217
  for (const f of runPageCheck('skipped-heading', () => checkPageQualityFromDoc(document))) {
210
218
  findings.push(finding(f.id, filePath, f.snippet));
211
219
  }
212
- for (const f of runPageCheck('html-patterns', () => checkHtmlPatterns(html).filter(item =>
220
+ // Scoped corpora for the pattern checks (see buildHtmlPatternCorpora in
221
+ // rules/checks.mjs): CSS-property regexes must not fire on prose ABOUT
222
+ // css — `<code>background-clip: text</code>` in a changelog is
223
+ // documentation, not styling. cssText already carries the <style>
224
+ // blocks and any linked local stylesheets; style/class attributes come
225
+ // from the parsed document, so escaped code samples never contribute.
226
+ const styleAttrParts = [];
227
+ const classAttrParts = [];
228
+ for (const el of document.querySelectorAll('*')) {
229
+ const styleAttr = el.getAttribute('style');
230
+ if (styleAttr) styleAttrParts.push(`style="${styleAttr}"`);
231
+ const classAttr = el.getAttribute('class');
232
+ if (classAttr) classAttrParts.push(classAttr);
233
+ }
234
+ const patternCorpora = {
235
+ styleText: [cssText, ...styleAttrParts].join('\n'),
236
+ classText: classAttrParts.join('\n'),
237
+ };
238
+ for (const f of runPageCheck('html-patterns', () => checkHtmlPatterns(html, patternCorpora).filter(item =>
213
239
  item.id !== 'bounce-easing' && item.id !== 'layout-transition'
214
240
  ))) {
215
- findings.push(finding(f.id, filePath, f.snippet));
241
+ const item = finding(f.id, filePath, f.snippet);
242
+ // Position-aware severity promotion: checks may attach a per-finding
243
+ // severity (e.g. a pulsing dot inside a header/nav landmark) that
244
+ // overrides the registry default.
245
+ if (f.severity) item.severity = f.severity;
246
+ findings.push(item);
216
247
  }
217
248
  // Text-content analyzers (em-dash overuse, marketing buzzwords,
218
249
  // numbered section markers, aphoristic cadence) live in the regex
@@ -224,11 +255,10 @@ async function detectHtml(filePath, options = {}) {
224
255
  }
225
256
  }
226
257
 
227
- const byProvider = filterByProviders(findings, options.providers);
228
258
  // Static-HTML findings carry no line number, so only whole-file
229
259
  // `impeccable-disable` directives apply here — exactly the standalone-document
230
260
  // waiver this primitive targets. Bypassed by `--no-config` / `--no-inline-ignores`.
231
- return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, html);
261
+ return options?.inlineIgnores === false ? findings : applyInlineIgnores(findings, html);
232
262
  }
233
263
 
234
264
  export { checkStaticPageTypography, STATIC_ELEMENT_RULES, detectHtml };
@@ -6,7 +6,13 @@ function getAP(id) {
6
6
 
7
7
  function finding(id, filePath, snippet, line = 0) {
8
8
  const ap = getAP(id);
9
- return { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', file: filePath, line, snippet };
9
+ const base = { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', category: ap.category || null, file: filePath, line, snippet };
10
+ // Advisory findings are detected but reported separately and never counted as
11
+ // failures. Carry the flag on the finding so every consumer (CLI, JSON, hook)
12
+ // can partition without a registry lookup. Only stamped when true to keep the
13
+ // finding shape stable for the vast majority of rules.
14
+ if (ap.advisory === true) base.advisory = true;
15
+ return base;
10
16
  }
11
17
 
12
18
  export { getAP, finding };
@@ -5,11 +5,24 @@ import path from 'node:path';
5
5
  // File walker
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
+ // Hidden directories are skipped wholesale during recursion (below), which
9
+ // covers .git / .next / .nuxt / .svelte-kit / .turbo / .vercel and — the
10
+ // issue #303 class — every vendored AI-harness install (.claude, .cursor,
11
+ // .codex, .agents, .impeccable, ...) whose bundled detector source would
12
+ // otherwise be reported as findings on a root scan. Only the non-hidden
13
+ // build/dependency dirs need naming. An explicitly passed hidden target
14
+ // still scans: walkDir name-checks children, never the root it's given.
8
15
  const SKIP_DIRS = new Set([
9
- 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output',
10
- '.svelte-kit', '__pycache__', '.turbo', '.vercel',
16
+ 'node_modules', 'dist', 'build', '__pycache__',
11
17
  ]);
12
18
 
19
+ // The exceptions to the hidden-dir rule: hidden directories that
20
+ // conventionally hold real UI source rather than tooling or vendored code.
21
+ // VitePress and VuePress keep custom theme components in
22
+ // .vitepress/theme/*.vue / .vuepress/theme/, and Storybook keeps preview
23
+ // decorators/styles in .storybook/.
24
+ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
25
+
13
26
  const SCANNABLE_EXTENSIONS = new Set([
14
27
  '.html', '.htm', '.css', '.scss', '.sass', '.less',
15
28
  '.jsx', '.tsx', '.js', '.ts',
@@ -24,6 +37,7 @@ function walkDir(dir) {
24
37
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
25
38
  for (const entry of entries) {
26
39
  if (SKIP_DIRS.has(entry.name)) continue;
40
+ if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
27
41
  const full = path.join(dir, entry.name);
28
42
  if (entry.isDirectory()) files.push(...walkDir(full));
29
43
  else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);