quiver-cli 1.3.0 → 1.3.1

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 (120) hide show
  1. package/package.json +1 -1
  2. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  3. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  4. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  5. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  6. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  7. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  8. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  9. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  10. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  12. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  13. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  14. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  15. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  16. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  17. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  18. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  19. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  20. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  21. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  22. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  23. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  24. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  25. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  26. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  28. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  29. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  30. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  31. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  32. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  33. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  34. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  36. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  37. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  39. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  40. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  41. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  42. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  43. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  45. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  47. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  48. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  49. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  51. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  56. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  57. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  58. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  60. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  61. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  62. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  63. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  64. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  65. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  66. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  67. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  68. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  69. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  70. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  75. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  76. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  83. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  84. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  85. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  86. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  89. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  90. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  91. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  92. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  94. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  95. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  96. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  97. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  98. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  99. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  100. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  101. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  102. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  103. package/template/.agents/skills/hono/SKILL.md +17 -3
  104. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  105. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  106. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  107. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  108. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  109. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  110. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  111. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  112. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  113. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  114. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  115. package/template/.agents/skills/supabase/SKILL.md +5 -1
  116. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  117. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  118. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  119. package/template/.agents/upstreams.json +21 -22
  120. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -226,6 +226,10 @@ const STATIC_INHERITED_PROPS = new Set([
226
226
  'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
227
227
  'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
228
228
  'webkitHyphens',
229
+ // visibility inherits in real CSS, and the invisible-at-rest contrast skip
230
+ // relies on descendants of a hidden container computing as hidden. A child
231
+ // that declares `visibility: visible` still overrides the inherited value.
232
+ 'visibility',
229
233
  ]);
230
234
 
231
235
  const STATIC_DEFAULT_STYLE = {
@@ -278,6 +282,7 @@ const STATIC_DEFAULT_STYLE = {
278
282
  marginLeft: '0px',
279
283
  position: 'static',
280
284
  visibility: 'visible',
285
+ opacity: '1',
281
286
  top: 'auto',
282
287
  right: 'auto',
283
288
  bottom: 'auto',
@@ -334,6 +339,7 @@ const STATIC_PROP_MAP = {
334
339
  'margin-left': 'marginLeft',
335
340
  'position': 'position',
336
341
  'visibility': 'visibility',
342
+ 'opacity': 'opacity',
337
343
  'top': 'top',
338
344
  'right': 'right',
339
345
  'bottom': 'bottom',
@@ -829,10 +835,11 @@ class StaticElement {
829
835
  }
830
836
  }
831
837
  closest(selector) {
838
+ const matcher = this._doc.matcherFor(selector);
832
839
  let cur = this.node;
833
840
  while (cur && cur.type === 'tag') {
834
841
  try {
835
- if (this._doc.is(cur, selector)) return this._doc.wrap(cur);
842
+ if (matcher(cur)) return this._doc.wrap(cur);
836
843
  } catch {
837
844
  return null;
838
845
  }
@@ -856,9 +863,10 @@ class StaticDocument {
856
863
  this.root = root;
857
864
  this.selectAll = modules.selectAll;
858
865
  this.selectOne = modules.selectOne;
859
- this.is = modules.is;
866
+ this.compile = modules.compile;
860
867
  this.domutils = modules.domutils;
861
868
  this._wrappers = new WeakMap();
869
+ this._compiledSelectors = new Map();
862
870
  this._styleMap = new WeakMap();
863
871
  this._hoverStyleMap = new WeakMap();
864
872
  this._accentDashPseudo = new WeakSet();
@@ -876,6 +884,20 @@ class StaticDocument {
876
884
  }
877
885
  return wrapped;
878
886
  }
887
+ matcherFor(selector) {
888
+ let matcher = this._compiledSelectors.get(selector);
889
+ if (!matcher) {
890
+ try {
891
+ matcher = this.compile(selector);
892
+ } catch (err) {
893
+ // Cache the failure as a rethrower so a bad selector still reaches
894
+ // closest()'s catch on every call, first and repeat alike.
895
+ matcher = () => { throw err; };
896
+ }
897
+ this._compiledSelectors.set(selector, matcher);
898
+ }
899
+ return matcher;
900
+ }
879
901
  querySelectorAll(selector) {
880
902
  try {
881
903
  return this.selectAll(selector, this.root.children || []).map(node => this.wrap(node));
@@ -942,8 +964,34 @@ function buildStaticWindow(staticDoc) {
942
964
  };
943
965
  }
944
966
 
967
+ function resolveLinkedCssPath(fileDir, href) {
968
+ const stripped = href.split(/[?#]/)[0];
969
+ const rootRelative = stripped.startsWith('/') && !stripped.startsWith('//');
970
+ if (!rootRelative) return path.resolve(fileDir, stripped);
971
+ // Drop "." and reject ".." so /../outside.css cannot walk out of dir.
972
+ const segments = stripped.replace(/^\/+/, '').split(/[/\\]/).filter(p => p && p !== '.');
973
+ if (segments.some(p => p === '..')) return path.join(fileDir, segments.filter(p => p !== '..').join(path.sep));
974
+ const rel = segments.join(path.sep);
975
+ let dir = fileDir;
976
+ for (;;) {
977
+ const parent = path.dirname(dir);
978
+ if (parent === dir) break; // never use the filesystem root as document root
979
+ try {
980
+ const candidate = path.join(dir, rel);
981
+ if (fs.statSync(candidate).isFile()) return candidate;
982
+ } catch { /* missing or unreadable candidate */ }
983
+ // Stop at the project root so a coincidental ~/static/app.css cannot win.
984
+ try {
985
+ if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))) break;
986
+ } catch { /* unreadable marker */ }
987
+ dir = parent;
988
+ }
989
+ return path.join(fileDir, rel);
990
+ }
991
+
945
992
  function collectStaticCssText(root, fileDir, profile, filePath, modules) {
946
993
  const styleTexts = [];
994
+ const warnedMissingStylesheets = new Set();
947
995
  for (const styleEl of modules.selectAll('style', root.children || [])) {
948
996
  styleTexts.push(modules.domutils.textContent(styleEl));
949
997
  }
@@ -952,10 +1000,10 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
952
1000
  const rel = link.attribs?.rel || '';
953
1001
  const href = link.attribs?.href || '';
954
1002
  if (!/\bstylesheet\b/i.test(rel) || !href || /^(https?:)?\/\//i.test(href)) continue;
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]);
1003
+ // Cache-busting (styles.css?v=3) and root-relative (/static/app.css) hrefs
1004
+ // must not resolve as OS-absolute paths; otherwise the whole stylesheet is
1005
+ // invisible to every element-level check.
1006
+ const cssPath = resolveLinkedCssPath(fileDir, href);
959
1007
  try {
960
1008
  const css = profileStep(profile, {
961
1009
  engine: 'static-html',
@@ -965,7 +1013,14 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
965
1013
  detail: href,
966
1014
  }, () => fs.readFileSync(cssPath, 'utf-8'));
967
1015
  styleTexts.push(css);
968
- } catch { /* skip unreadable */ }
1016
+ } catch {
1017
+ if (!warnedMissingStylesheets.has(cssPath)) {
1018
+ warnedMissingStylesheets.add(cssPath);
1019
+ process.stderr.write(
1020
+ `impeccable detect: could not read linked stylesheet ${href} (resolved to ${cssPath}); color and custom-property rules will be incomplete\n`
1021
+ );
1022
+ }
1023
+ }
969
1024
  }
970
1025
  return styleTexts.join('\n');
971
1026
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
4
+ import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
5
5
  import {
6
6
  checkSourceDesignSystem,
7
7
  collectStaticDesignSystemFindings,
@@ -28,6 +28,7 @@ import {
28
28
  checkCreamPalette,
29
29
  checkHtmlPatterns,
30
30
  checkKickerAboveHeadingFromDoc,
31
+ scopedIgnoreActive,
31
32
  checkNumberedSectionLabelsFromDoc,
32
33
  checkPageLayout,
33
34
  checkPageQualityFromDoc,
@@ -50,9 +51,7 @@ function checkStaticPageTypography(document, window) {
50
51
  for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
51
52
  const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
52
53
  if (!hasText) continue;
53
- const ff = window.getComputedStyle(el).fontFamily || '';
54
- const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
55
- const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
54
+ const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
56
55
  if (!primary) continue;
57
56
  fonts.add(primary);
58
57
  if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -133,15 +132,26 @@ async function detectHtml(filePath, options = {}) {
133
132
  parseDocument: htmlparser2.parseDocument,
134
133
  selectAll: cssSelect.selectAll,
135
134
  selectOne: cssSelect.selectOne,
136
- is: cssSelect.is,
135
+ compile: cssSelect.compile,
137
136
  csstree,
138
137
  domutils,
139
138
  };
140
139
  });
141
- } catch {
142
- return detectText(html, filePath, options);
140
+ } catch (err) {
141
+ if (!globalThis.__impeccableStaticHtmlWarned) {
142
+ globalThis.__impeccableStaticHtmlWarned = true;
143
+
144
+ process.stderr.write(
145
+ 'impeccable detect: DEGRADED - HTML parser modules unavailable ' +
146
+ '(htmlparser2, css-select, css-tree, domutils).\n' +
147
+ 'Falling back to regex matching. Custom properties, selector matching and computed ' +
148
+ 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n'
149
+ );
143
150
  }
144
151
 
152
+ return detectText(html, filePath, options);
153
+ }
154
+
145
155
  const resolvedPath = path.resolve(filePath);
146
156
  const fileDir = path.dirname(resolvedPath);
147
157
  const root = profileStep(profile, {
@@ -171,6 +181,9 @@ async function detectHtml(filePath, options = {}) {
171
181
  const tag = el.tagName.toLowerCase();
172
182
  const style = window.getComputedStyle(el);
173
183
  for (const f of runElementCheck(rule.id, () => rule.run(el, tag, style, window, customPropMap))) {
184
+ // Element-scoped waivers: a data-impeccable-ignore ancestor suppresses
185
+ // matching findings for its subtree, same as the browser walk.
186
+ if (scopedIgnoreActive(el, f.id)) continue;
174
187
  findings.push(finding(f.id, filePath, f.snippet));
175
188
  }
176
189
  }
@@ -238,6 +251,17 @@ async function detectHtml(filePath, options = {}) {
238
251
  for (const f of runPageCheck('html-patterns', () => checkHtmlPatterns(html, patternCorpora).filter(item =>
239
252
  item.id !== 'bounce-easing' && item.id !== 'layout-transition'
240
253
  ))) {
254
+ // Selector-backed page findings honor scoped waivers here too, matching
255
+ // the browser pass: resolve the selector and drop the finding when an
256
+ // ignoring ancestor covers a match. Unlike the browser, an unmatched
257
+ // selector keeps the finding — static scans see partial documents.
258
+ if (f.selector) {
259
+ let matches = null;
260
+ try {
261
+ matches = document.querySelectorAll(String(f.selector).replace(/::?[a-zA-Z-]+(\([^)]*\))?/g, '').trim());
262
+ } catch { matches = null; }
263
+ if (matches && matches.length > 0 && [...matches].every(el => scopedIgnoreActive(el, f.id))) continue;
264
+ }
241
265
  const item = finding(f.id, filePath, f.snippet);
242
266
  // Position-aware severity promotion: checks may attach a per-finding
243
267
  // severity (e.g. a pulsing dot inside a header/nav landmark) that
@@ -26,11 +26,26 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
26
26
  const SCANNABLE_EXTENSIONS = new Set([
27
27
  '.html', '.htm', '.css', '.scss', '.sass', '.less',
28
28
  '.jsx', '.tsx', '.js', '.ts',
29
- '.vue', '.svelte', '.astro',
29
+ '.vue', '.svelte', '.astro', '.blade.php',
30
30
  ]);
31
31
 
32
32
  const HTML_EXTENSIONS = new Set(['.html', '.htm']);
33
33
 
34
+ function hasScannableExtension(filename) {
35
+ const lower = filename.toLowerCase();
36
+ if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
37
+ for (const ext of SCANNABLE_EXTENSIONS) {
38
+ if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ const IMPORT_SPECIFIER_PATTERNS = [
44
+ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
45
+ /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
46
+ /@(?:use|forward)\s+['"]([^'"]+)['"]/g,
47
+ ];
48
+
34
49
  function walkDir(dir) {
35
50
  const files = [];
36
51
  let entries;
@@ -40,7 +55,7 @@ function walkDir(dir) {
40
55
  if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
41
56
  const full = path.join(dir, entry.name);
42
57
  if (entry.isDirectory()) files.push(...walkDir(full));
43
- else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
58
+ else if (hasScannableExtension(entry.name)) files.push(full);
44
59
  }
45
60
  return files;
46
61
  }
@@ -75,26 +90,11 @@ function buildImportGraph(files) {
75
90
  const dir = path.dirname(file);
76
91
  const imports = new Set();
77
92
 
78
- // ES imports: import ... from '...' and import '...'
79
- const esRe = /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g;
80
- let m;
81
- while ((m = esRe.exec(content)) !== null) {
82
- const resolved = resolveImport(m[1], dir, fileSet);
83
- if (resolved) imports.add(resolved);
84
- }
85
-
86
- // CSS @import
87
- const cssRe = /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g;
88
- while ((m = cssRe.exec(content)) !== null) {
89
- const resolved = resolveImport(m[1], dir, fileSet);
90
- if (resolved) imports.add(resolved);
91
- }
92
-
93
- // SCSS @use / @forward
94
- const scssRe = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
95
- while ((m = scssRe.exec(content)) !== null) {
96
- const resolved = resolveImport(m[1], dir, fileSet);
97
- if (resolved) imports.add(resolved);
93
+ for (const pattern of IMPORT_SPECIFIER_PATTERNS) {
94
+ for (const match of content.matchAll(pattern)) {
95
+ const resolved = resolveImport(match[1], dir, fileSet);
96
+ if (resolved) imports.add(resolved);
97
+ }
98
98
  }
99
99
 
100
100
  graph.set(file, imports);
@@ -203,6 +203,7 @@ export {
203
203
  SKIP_DIRS,
204
204
  SCANNABLE_EXTENSIONS,
205
205
  HTML_EXTENSIONS,
206
+ hasScannableExtension,
206
207
  walkDir,
207
208
  resolveImport,
208
209
  buildImportGraph,
@@ -121,6 +121,24 @@ const ANTIPATTERNS = [
121
121
  'A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.',
122
122
  skillSection: 'Imagery',
123
123
  },
124
+ {
125
+ id: 'organic-clip-path',
126
+ category: 'quality',
127
+ name: 'Organic contour drawn as clip-path',
128
+ description:
129
+ 'A clip-path polygon with many arbitrary vertices, or a curved clip-path path(), is CSS approximating a torn edge, blob, or silhouette. It reads as the cheap version of the effect and is usually a produced or photographic material replaced with code. Derive an alpha matte from the real image, or ship the shape as a cut-out raster; keep clip-path for geometry (cut corners, diagonals, hexagons).',
130
+ skillSection: 'Imagery',
131
+ skillGuideline: 'geometric masks standing in for organic contours',
132
+ },
133
+ {
134
+ id: 'buried-raster',
135
+ category: 'quality',
136
+ name: 'Raster buried under a wash or opacity',
137
+ description:
138
+ 'A background image under a near-opaque gradient wash, or a raster on an element at near-zero opacity, never reaches the screen: the page shows the wash, and the produced texture or photo ships as a compliance token. Let the material show (a tint under 0.9 alpha, a blend mode, an opacity you can see) or remove the file.',
139
+ skillSection: 'Imagery',
140
+ skillGuideline: 'a produced material must survive to the screen',
141
+ },
124
142
  {
125
143
  id: 'dark-glow',
126
144
  category: 'slop',