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,11 +2,11 @@
2
2
  /**
3
3
  * Critique persistence helper.
4
4
  *
5
- * Each run of /impeccable critique writes a per-target snapshot to
5
+ * Each critique run writes a per-target snapshot to
6
6
  * .impeccable/critique/<timestamp>__<slug>.md
7
7
  * with a small YAML frontmatter carrying the score + P0/P1 counts.
8
8
  *
9
- * /impeccable polish reads the latest matching snapshot at start as its
9
+ * The polish workflow reads the latest matching snapshot at start as its
10
10
  * fix backlog. No other skill auto-reads critique output.
11
11
  *
12
12
  * The slug is derived mechanically from the *resolved* primary artifact
@@ -29,8 +29,9 @@ import fs from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
  import { getCritiqueDir } from './lib/impeccable-paths.mjs';
32
+ import { slugFromTarget } from './lib/target-slug.mjs';
32
33
 
33
- const SLUG_MAX = 50;
34
+ export { slugFromTarget } from './lib/target-slug.mjs';
34
35
 
35
36
  /**
36
37
  * Mechanically derive a slug from a resolved target. Returns null if the
@@ -40,46 +41,6 @@ const SLUG_MAX = 50;
40
41
  * concrete artifact before calling this — we never slug a natural-language
41
42
  * phrase.
42
43
  */
43
- export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
44
- if (!resolved || typeof resolved !== 'string') return null;
45
- const trimmed = resolved.trim();
46
- if (!trimmed) return null;
47
-
48
- // URL
49
- if (/^https?:\/\//i.test(trimmed)) {
50
- let url;
51
- try { url = new URL(trimmed); } catch { return null; }
52
- const hostPath = `${url.hostname}${url.pathname}`;
53
- return kebab(hostPath);
54
- }
55
-
56
- // File path. Make it project-relative so two devs critiquing the same
57
- // checkout get the same slug regardless of where their repo is cloned.
58
- const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
59
- let rel = path.relative(cwd, abs);
60
- // If the target is outside cwd, fall back to the basename so we still
61
- // produce a stable slug (vs the absolute path, which would include
62
- // home dirs / usernames).
63
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
64
- rel = path.basename(abs);
65
- }
66
- if (!rel || rel === '.' || rel === '') return null;
67
- return kebab(rel);
68
- }
69
-
70
- function kebab(s) {
71
- const slug = s
72
- .toLowerCase()
73
- .replace(/[/\\.]+/g, '-')
74
- .replace(/[^a-z0-9-]+/g, '-')
75
- .replace(/-+/g, '-')
76
- .replace(/^-|-$/g, '');
77
- if (!slug) return null;
78
- // Cap from the tail — the tail (filename) is more identifying than the
79
- // top-level directory.
80
- return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
81
- }
82
-
83
44
  /**
84
45
  * Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
85
46
  * Plain colons aren't allowed on Windows filesystems.
@@ -180,6 +141,15 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
180
141
 
181
142
  // ---- CLI ---------------------------------------------------------------
182
143
 
144
+ // Accept either a ready slug or a concrete target (path/URL) everywhere, so
145
+ // callers never have to run the slug step separately. Anything containing a
146
+ // path or URL marker is resolved through slugFromTarget.
147
+ function coerceSlug(value) {
148
+ if (!value) return null;
149
+ if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
150
+ return slugFromTarget(value);
151
+ }
152
+
183
153
  function main(argv) {
184
154
  const [cmd, ...args] = argv;
185
155
  switch (cmd) {
@@ -190,8 +160,9 @@ function main(argv) {
190
160
  return;
191
161
  }
192
162
  case 'write': {
193
- const [slug, bodyFile] = args;
194
- if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
163
+ const [slugArg, bodyFile] = args;
164
+ const slug = coerceSlug(slugArg);
165
+ if (!slug || !bodyFile) { process.stderr.write('usage: write <slug-or-target> <body-file>\n'); process.exit(1); }
195
166
  const raw = fs.readFileSync(bodyFile, 'utf-8');
196
167
  // The body file may be a full report. The caller passes the meta as
197
168
  // a JSON object on stdin if it wants structured frontmatter; otherwise
@@ -206,13 +177,13 @@ function main(argv) {
206
177
  return;
207
178
  }
208
179
  case 'latest': {
209
- const latest = readLatestSnapshot(args[0]);
180
+ const latest = readLatestSnapshot(coerceSlug(args[0]));
210
181
  if (!latest) { process.exit(2); }
211
182
  process.stdout.write(latest.body);
212
183
  return;
213
184
  }
214
185
  case 'trend': {
215
- const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
186
+ const rows = readTrend(coerceSlug(args[0]), { limit: args[1] ? Number(args[1]) : 5 });
216
187
  process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
217
188
  return;
218
189
  }
@@ -530,7 +530,11 @@ if (IS_BROWSER) {
530
530
  function generateSelector(el) {
531
531
  if (el === document.body) return 'body';
532
532
  if (el === document.documentElement) return 'html';
533
- if (el.id) return '#' + CSS.escape(el.id);
533
+ // Read via getAttribute when `el.id` is not a string — a <form> with a
534
+ // named control (e.g. <input name="id">) shadows the builtin getter and
535
+ // returns the element, producing a garbage `#[object …]` selector (#407).
536
+ const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
537
+ if (elId) return '#' + CSS.escape(elId);
534
538
 
535
539
  const parts = [];
536
540
  let current = el;
@@ -1222,7 +1226,11 @@ if (IS_BROWSER) {
1222
1226
  return {
1223
1227
  type: f.type || f.id,
1224
1228
  category: ap ? ap.category : 'quality',
1225
- severity: ap?.severity || 'warning',
1229
+ severity: f.severity || ap?.severity || 'warning',
1230
+ // Advisory findings (em-dash overuse, etc.) are surfaced but never
1231
+ // treated as failures; carry the flag so the overlay/extension can
1232
+ // render them with the mildest affordance and consumers can filter.
1233
+ advisory: (ap && ap.advisory === true) || f.advisory === true,
1226
1234
  detail: f.detail || f.snippet,
1227
1235
  ignoreValue: f.ignoreValue || f.value || '',
1228
1236
  name: ap ? ap.name : (f.type || f.id),
@@ -1458,16 +1466,16 @@ if (IS_BROWSER) {
1458
1466
  const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
1459
1467
  const designSystem = browserDesignSystemConfig();
1460
1468
  const designSeen = { fonts: new Set(), colors: new Set(), radii: new Set() };
1461
- // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a
1462
- // real browser env (detector page, live overlay, extension) running every
1463
- // check is free, so we always surface them; the gating is purely a CLI
1464
- // output concern, applied in the Node engines' detect* return paths.
1469
+ // All deterministic rules run in the browser and extension path.
1465
1470
 
1466
1471
  for (const el of document.querySelectorAll('*')) {
1467
1472
  // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
1468
1473
  if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
1469
- // Skip browser extension elements (Claude, etc.)
1470
- const elId = el.id || '';
1474
+ // Skip browser extension elements (Claude, etc.). Use getAttribute when
1475
+ // `el.id` is not a string: a <form> with a named control like
1476
+ // <input name="id"> shadows the builtin `id` getter and returns the
1477
+ // element, whose `.startsWith` throws (issue #407).
1478
+ const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
1471
1479
  if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
1472
1480
  // Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
1473
1481
  // These are inspector chrome, not part of the user's design.
@@ -1477,10 +1485,12 @@ if (IS_BROWSER) {
1477
1485
 
1478
1486
  const findings = [
1479
1487
  ...checkElementBordersDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1488
+ ...checkElementPseudoStripeDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1480
1489
  ...checkElementColorsDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1481
1490
  ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1482
1491
  ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1483
1492
  ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1493
+ ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1484
1494
  ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1485
1495
  ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1486
1496
  ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
@@ -1488,6 +1498,7 @@ if (IS_BROWSER) {
1488
1498
  ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1489
1499
  ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1490
1500
  ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1501
+ ...checkElementBlinkingCursorDOM(el).map(f => ({ type: f.id, detail: f.snippet, ...(f.severity ? { severity: f.severity } : {}) })),
1491
1502
  ...checkElementDesignSystemDOM(el, designSystem, designSeen),
1492
1503
  ].filter(f => _ruleOk(f.type));
1493
1504
 
@@ -1518,7 +1529,7 @@ if (IS_BROWSER) {
1518
1529
  addBrowserFindings(groupMap, document.body, typoFindings);
1519
1530
  }
1520
1531
 
1521
- const sectionKickerFindings = checkRepeatedSectionKickersDOM()
1532
+ const sectionKickerFindings = checkKickerAboveHeadingDOM()
1522
1533
  .map(f => ({ type: f.id, detail: f.snippet }))
1523
1534
  .filter(f => _ruleOk(f.type));
1524
1535
  if (sectionKickerFindings.length > 0) {
@@ -1526,12 +1537,66 @@ if (IS_BROWSER) {
1526
1537
  addBrowserFindings(groupMap, document.body, sectionKickerFindings);
1527
1538
  }
1528
1539
 
1540
+ const numberedLabelFindings = checkNumberedSectionLabelsDOM()
1541
+ .map(f => ({ type: f.id, detail: f.snippet }))
1542
+ .filter(f => _ruleOk(f.type));
1543
+ if (numberedLabelFindings.length > 0) {
1544
+ pageLevelFindings.push(...numberedLabelFindings);
1545
+ addBrowserFindings(groupMap, document.body, numberedLabelFindings);
1546
+ }
1547
+
1548
+ const repeatedTextFindings = checkRepeatedContainerTextDOM()
1549
+ .map(f => ({ type: f.id, detail: f.snippet }))
1550
+ .filter(f => _ruleOk(f.type));
1551
+ if (repeatedTextFindings.length > 0) {
1552
+ pageLevelFindings.push(...repeatedTextFindings);
1553
+ addBrowserFindings(groupMap, document.body, repeatedTextFindings);
1554
+ }
1555
+
1556
+ // Em-dash overuse (advisory): browser parity with the static/regex path.
1557
+ // Reads rendered body text so it catches dashes written as HTML entities.
1558
+ // serializeFindings stamps the advisory flag from the registry.
1559
+ const emDashFindings = checkEmDashOveruseDOM()
1560
+ .map(f => ({ type: f.id, detail: f.snippet }))
1561
+ .filter(f => _ruleOk(f.type));
1562
+ if (emDashFindings.length > 0) {
1563
+ pageLevelFindings.push(...emDashFindings);
1564
+ addBrowserFindings(groupMap, document.body, emDashFindings);
1565
+ }
1566
+
1529
1567
  const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
1530
1568
  for (const f of layoutFindings) {
1531
1569
  const el = f.el || document.body;
1532
1570
  addBrowserFindings(groupMap, el, [{ type: f.type, detail: f.detail || f.snippet }]);
1533
1571
  }
1534
1572
 
1573
+ // Heading rhythm (browser-only: needs real layout for the gap math)
1574
+ const headingRhythmFindings = checkHeadingRhythmDOM().filter(f => _ruleOk(f.type));
1575
+ for (const f of headingRhythmFindings) {
1576
+ addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
1577
+ }
1578
+
1579
+ // Edge-flush cards in horizontal scrollers (browser-only: needs real
1580
+ // layout for the scroller clip box vs card rect math)
1581
+ const edgeFlushFindings = checkEdgeFlushCardsDOM().filter(f => _ruleOk(f.type));
1582
+ for (const f of edgeFlushFindings) {
1583
+ addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
1584
+ }
1585
+
1586
+ // Text occlusion / element overlap (browser-only: needs real layout +
1587
+ // elementFromPoint to confirm what actually paints on top)
1588
+ const occlusionFindings = checkTextOcclusionDOM().filter(f => _ruleOk(f.type));
1589
+ for (const f of occlusionFindings) {
1590
+ addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
1591
+ }
1592
+
1593
+ // First-viewport column overflow — the stretched-hero signature
1594
+ // (browser-only: needs real layout for the content-extent math)
1595
+ const colOverflowFindings = checkFirstViewportColumnOverflowDOM().filter(f => _ruleOk(f.type));
1596
+ for (const f of colOverflowFindings) {
1597
+ addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
1598
+ }
1599
+
1535
1600
  // Page-level quality checks (headings, etc.)
1536
1601
  const qualityFindings = checkPageQualityDOM().filter(f => _ruleOk(f.type));
1537
1602
  if (qualityFindings.length > 0) {
@@ -1557,7 +1622,25 @@ if (IS_BROWSER) {
1557
1622
  }
1558
1623
  const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
1559
1624
  if (htmlPatternFindings.length > 0) {
1560
- const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type));
1625
+ const mapped = htmlPatternFindings.map(f => {
1626
+ const item = { type: f.id, detail: f.snippet };
1627
+ if (f.severity) {
1628
+ item.severity = f.severity;
1629
+ } else if (f.id === 'pulsing-dot' && f.selector) {
1630
+ // The string scan promotes header/nav dots on its own; with a live
1631
+ // layout also promote dots resting in the first ~900px of the page
1632
+ // (the hero region), which the source scan cannot measure.
1633
+ try {
1634
+ const dotEl = document.querySelector(f.selector);
1635
+ if (dotEl) {
1636
+ const rect = dotEl.getBoundingClientRect();
1637
+ const pageTop = rect.top + (window.scrollY || 0);
1638
+ if (pageTop <= 900) item.severity = 'error';
1639
+ }
1640
+ } catch { /* unresolvable selector: keep registry severity */ }
1641
+ }
1642
+ return item;
1643
+ }).filter(f => _ruleOk(f.type));
1561
1644
  pageLevelFindings.push(...mapped);
1562
1645
  addBrowserFindings(groupMap, document.body, mapped);
1563
1646
  }
@@ -1931,6 +2014,9 @@ if (IS_BROWSER) {
1931
2014
  window.impeccableDetectAsync = detectAsync;
1932
2015
  window.impeccableScan = scan;
1933
2016
  window.impeccableScanAsync = scanAsync;
2017
+ // Raw measurement for the URL engine's content-hidden-at-rest pass: it
2018
+ // drives a reveal sweep from Node and thresholds the result itself.
2019
+ window.impeccableMeasureHiddenText = measureHiddenTextDOM;
1934
2020
  window.impeccableCollectVisualContrastCandidates = collectVisualContrastCandidates;
1935
2021
  window.impeccableAnalyzeVisualContrast = analyzeVisualContrast;
1936
2022
  window.impeccableGetLastVisualContrastAnalyses = () => lastVisualContrastAnalyses.slice();
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
 
4
- import { loadDesignSystemForCwd } from '../design-system.mjs';
5
+ import { loadDesignSystemForTarget } from '../design-system.mjs';
6
+ import { RULE_SCOPES, filterByScopes } from '../registry/antipatterns.mjs';
5
7
  import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs';
6
8
  import { detectHtml } from '../engines/static-html/detect-html.mjs';
7
9
  import { detectText } from '../engines/regex/detect-text.mjs';
@@ -26,9 +28,37 @@ function formatFindingSummary(count) {
26
28
  return `${count} anti-pattern${count === 1 ? '' : 's'} found.`;
27
29
  }
28
30
 
29
- function formatFindings(findings, jsonMode) {
30
- if (jsonMode) return JSON.stringify(findings, null, 2);
31
+ // Local filesystem path behind a file:// URL, or null when it can't be mapped.
32
+ function fileUrlToLocalPath(url) {
33
+ try {
34
+ return fileURLToPath(url);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ // Advisory findings are detected but never treated as failures: they list in a
41
+ // separate, visually dimmed section, are excluded from the failure count that
42
+ // drives the exit code, and carry `"advisory": true` in JSON so consumers can
43
+ // filter. Every advisory finding carries the flag (stamped by the registry via
44
+ // findings.mjs).
45
+ function isAdvisory(finding) {
46
+ return finding && finding.advisory === true;
47
+ }
48
+
49
+ function partitionAdvisory(findings) {
50
+ const primary = [];
51
+ const advisory = [];
52
+ for (const f of findings) (isAdvisory(f) ? advisory : primary).push(f);
53
+ return { primary, advisory };
54
+ }
55
+
56
+ // ANSI dim, when stderr is a TTY. Advisory output is chrome, so keep it quiet.
57
+ function dim(text) {
58
+ return process.stderr.isTTY ? `\x1b[2m${text}\x1b[0m` : text;
59
+ }
31
60
 
61
+ function formatFindingsBody(findings) {
32
62
  const grouped = {};
33
63
  for (const f of findings) {
34
64
  if (!grouped[f.file]) grouped[f.file] = [];
@@ -43,7 +73,28 @@ function formatFindings(findings, jsonMode) {
43
73
  out.push(` → ${item.description}`);
44
74
  }
45
75
  }
46
- out.push(`\n${formatFindingSummary(findings.length)}`);
76
+ return out;
77
+ }
78
+
79
+ function formatAdvisorySection(advisory) {
80
+ if (!advisory || advisory.length === 0) return '';
81
+ const lines = [`\n${dim('── Advisory (not counted as failures) ──')}`];
82
+ for (const line of formatFindingsBody(advisory)) lines.push(dim(line));
83
+ lines.push(dim(`\n${advisory.length} advisory note${advisory.length === 1 ? '' : 's'}. Suppress with --no-advisory.`));
84
+ return lines.join('\n');
85
+ }
86
+
87
+ // Text/JSON formatter. `findings` is the full set; advisory items are separated
88
+ // out into their own section and excluded from the failure summary count. JSON
89
+ // output keeps every finding (each advisory one flagged) in a single array.
90
+ function formatFindings(findings, jsonMode) {
91
+ if (jsonMode) return JSON.stringify(findings, null, 2);
92
+
93
+ const { primary, advisory } = partitionAdvisory(findings);
94
+ const out = [...formatFindingsBody(primary)];
95
+ out.push(`\n${formatFindingSummary(primary.length)}`);
96
+ const advisorySection = formatAdvisorySection(advisory);
97
+ if (advisorySection) out.push(advisorySection);
47
98
  return out.join('\n');
48
99
  }
49
100
 
@@ -51,7 +102,11 @@ function formatFindings(findings, jsonMode) {
51
102
  // Stdin handling
52
103
  // ---------------------------------------------------------------------------
53
104
 
54
- async function handleStdin(options = {}) {
105
+ // `optionsFor` maps a local path to scan options carrying that path's own
106
+ // project design system (or base options when null). Falls back to a plain
107
+ // object so direct/legacy callers still work.
108
+ async function handleStdin(optionsFor = () => ({})) {
109
+ const resolve = typeof optionsFor === 'function' ? optionsFor : () => optionsFor;
55
110
  const chunks = [];
56
111
  for await (const chunk of process.stdin) chunks.push(chunk);
57
112
  const input = Buffer.concat(chunks).toString('utf-8');
@@ -59,11 +114,12 @@ async function handleStdin(options = {}) {
59
114
  const parsed = JSON.parse(input);
60
115
  const fp = parsed?.tool_input?.file_path;
61
116
  if (fp && fs.existsSync(fp)) {
117
+ const options = resolve(fp);
62
118
  return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())
63
119
  ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);
64
120
  }
65
121
  } catch { /* not JSON */ }
66
- return detectText(input, '<stdin>', options);
122
+ return detectText(input, '<stdin>', resolve(null));
67
123
  }
68
124
 
69
125
 
@@ -91,14 +147,22 @@ Scan files or URLs for UI anti-patterns and design quality issues.
91
147
  Options:
92
148
  --json Output results as JSON
93
149
  --quiet In text mode, only print the final findings count
94
- --gpt Also report GPT-specific provider tells (off by default)
95
- --gemini Also report Gemini-specific provider tells (off by default)
150
+ --scope <name> Only report rules in the given design domain
151
+ (type, layout). Comma-separated.
152
+ --viewport <WxH> Browser viewport for URL scans (default 1280x800),
153
+ e.g. --viewport 390x844 for a mobile-width pass
96
154
  --no-config Do not apply project config, detector ignores, inline
97
155
  ignore comments, or DESIGN.md
98
156
  --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments
99
157
  --no-design-system Do not load local DESIGN.md / .impeccable/design.json context
158
+ --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)
100
159
  --help Show this help message
101
160
 
161
+ Advisory findings:
162
+ Some rules are advisory: detected and listed in a separate section, but never
163
+ counted as failures and never changing the exit code. They stay out of the
164
+ failure count so they never block automation. --no-advisory hides them.
165
+
102
166
  Project config:
103
167
  Respects .impeccable/config.json and .impeccable/config.local.json detector
104
168
  settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
@@ -115,7 +179,8 @@ Inline ignores:
115
179
  Detection modes:
116
180
  HTML files Static HTML/CSS analysis (default, catches linked CSS)
117
181
  Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
118
- URLs Puppeteer full browser rendering (auto-detected)
182
+ URLs Puppeteer full browser rendering (auto-detected;
183
+ http(s):// and file:// URLs)
119
184
 
120
185
  Examples:
121
186
  impeccable detect src/
@@ -135,6 +200,7 @@ async function detectCli() {
135
200
  const jsonMode = args.includes('--json');
136
201
  const quietMode = args.includes('--quiet');
137
202
  const helpMode = args.includes('--help');
203
+ const noAdvisory = args.includes('--no-advisory');
138
204
  // --fast (regex-only) is deprecated: since the jsdom removal, the static
139
205
  // HTML/CSS analysis is fast and covers every rule, so the regex-only path
140
206
  // only loses coverage for no real speed win. Accept the flag for back-compat
@@ -144,21 +210,74 @@ async function detectCli() {
144
210
  'Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n',
145
211
  );
146
212
  }
213
+ if (args.includes('--gpt') || args.includes('--gemini')) {
214
+ process.stderr.write(
215
+ 'Note: --gpt and --gemini are deprecated and ignored. Generated-UI tells now run by default.\n',
216
+ );
217
+ }
147
218
  const configEnabled = !args.includes('--no-config');
148
219
  const detectionConfig = configEnabled
149
220
  ? readDetectionConfig(process.cwd())
150
221
  : { ignoreRules: [], ignoreFiles: [], ignoreValues: [] };
151
- const providers = [];
152
- if (args.includes('--gpt')) providers.push('gpt');
153
- if (args.includes('--gemini')) providers.push('gemini');
222
+ const scopes = [];
223
+ for (let i = 0; i < args.length; i++) {
224
+ if (args[i] !== '--scope' && !args[i].startsWith('--scope=')) continue;
225
+ const inline = args[i].startsWith('--scope=');
226
+ const value = inline ? args[i].slice('--scope='.length) : args[i + 1];
227
+ const parsed = (value && !value.startsWith('--'))
228
+ ? value.split(',').map(s => s.trim()).filter(Boolean)
229
+ : [];
230
+ // A bare `--scope` would otherwise fall out of `targets` and scan unscoped;
231
+ // fail loudly so a mistyped pre-scan never runs the wrong rule set.
232
+ if (parsed.length === 0) {
233
+ process.stderr.write(
234
+ `Error: --scope requires a value. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`,
235
+ );
236
+ process.exit(1);
237
+ }
238
+ scopes.push(...parsed);
239
+ args.splice(i, inline ? 1 : 2);
240
+ i -= 1;
241
+ }
242
+ let viewport = null;
243
+ for (let i = 0; i < args.length; i++) {
244
+ if (args[i] !== '--viewport' && !args[i].startsWith('--viewport=')) continue;
245
+ const inline = args[i].startsWith('--viewport=');
246
+ const value = inline ? args[i].slice('--viewport='.length) : args[i + 1];
247
+ const match = /^(\d{2,5})x(\d{2,5})$/i.exec(value || '');
248
+ if (!match) {
249
+ process.stderr.write('Error: --viewport requires a WxH value, e.g. --viewport 390x844\n');
250
+ process.exit(1);
251
+ }
252
+ viewport = { width: Number(match[1]), height: Number(match[2]) };
253
+ args.splice(i, inline ? 1 : 2);
254
+ i -= 1;
255
+ }
256
+ const unknownScopes = scopes.filter(s => !RULE_SCOPES.has(s));
257
+ if (unknownScopes.length > 0) {
258
+ process.stderr.write(
259
+ `Error: unknown --scope value(s): ${unknownScopes.join(', ')}. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`,
260
+ );
261
+ process.exit(1);
262
+ }
154
263
  const designSystemEnabled = configEnabled && !args.includes('--no-design-system') && detectionConfig.designSystem?.enabled !== false;
155
- const designSystem = designSystemEnabled ? loadDesignSystemForCwd(process.cwd()) : null;
156
264
  // Inline `impeccable-disable*` waivers are part of the scanned file, so they
157
265
  // apply by default. `--no-config` (raw scan) and the dedicated
158
266
  // `--no-inline-ignores` both turn them off.
159
267
  const inlineIgnoresEnabled = configEnabled && !args.includes('--no-inline-ignores');
160
- const scanOptions = { providers, inlineIgnores: inlineIgnoresEnabled };
161
- if (designSystem) scanOptions.designSystem = designSystem;
268
+ const baseScanOptions = { inlineIgnores: inlineIgnoresEnabled };
269
+ if (viewport) baseScanOptions.viewport = viewport;
270
+ // DESIGN.md must resolve from EACH scan target's own project root, not from
271
+ // process.cwd(): scanning project B's files from inside project A applied A's
272
+ // design rules (cross-project contamination). Resolve per target, memoized by
273
+ // resolved project root so a multi-file scan pays the read once per project.
274
+ // A target with no project marker above it gets no design system (never cwd's).
275
+ const designSystemCache = new Map();
276
+ const scanOptionsFor = (localPath) => {
277
+ if (!designSystemEnabled || !localPath) return baseScanOptions;
278
+ const designSystem = loadDesignSystemForTarget(localPath, { cache: designSystemCache });
279
+ return designSystem ? { ...baseScanOptions, designSystem } : baseScanOptions;
280
+ };
162
281
  const targets = args.filter(a => !a.startsWith('--'));
163
282
 
164
283
  if (helpMode) { printUsage(); process.exit(0); }
@@ -166,19 +285,31 @@ async function detectCli() {
166
285
  let allFindings = [];
167
286
 
168
287
  if (!process.stdin.isTTY && targets.length === 0) {
169
- allFindings = await handleStdin(scanOptions);
288
+ allFindings = await handleStdin(scanOptionsFor);
170
289
  } else {
171
290
  const paths = targets.length > 0 ? targets : [process.cwd()];
172
- const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length;
291
+ // file:// URLs get the same Puppeteer-rendered pass as http(s) — the
292
+ // real cascade, real computed styles, real layout. Callers that want a
293
+ // browser-grade scan of a local artifact can pass file:///abs/path.html
294
+ // instead of the bare path (which stays on the static engine).
295
+ const urlRe = /^(?:https?|file):\/\//i;
296
+ const urlTargetCount = paths.filter(target => urlRe.test(target)).length;
173
297
  const browserDetector = urlTargetCount > 1 ? await createBrowserDetector() : null;
174
298
 
175
299
  try {
176
300
  for (const target of paths) {
177
- if (/^https?:\/\//i.test(target)) {
301
+ if (urlRe.test(target)) {
302
+ // A file:// URL points at a local artifact, so its design system
303
+ // resolves from that file's project. A remote http(s) URL has no
304
+ // local project — it gets base options (no design system), never
305
+ // process.cwd()'s.
306
+ const urlOptions = /^file:/i.test(target)
307
+ ? scanOptionsFor(fileUrlToLocalPath(target))
308
+ : baseScanOptions;
178
309
  try {
179
310
  const scanner = browserDetector
180
- ? (url) => browserDetector.detectUrl(url, scanOptions)
181
- : (url) => detectUrl(url, scanOptions);
311
+ ? (url) => browserDetector.detectUrl(url, urlOptions)
312
+ : (url) => detectUrl(url, urlOptions);
182
313
  allFindings.push(...await scanner(target));
183
314
  } catch (e) { process.stderr.write(`Error: ${e.message}\n`); }
184
315
  continue;
@@ -244,11 +375,14 @@ async function detectCli() {
244
375
 
245
376
  for (const file of files) {
246
377
  const ext = path.extname(file).toLowerCase();
378
+ // Each file resolves its own project design system (cached by root),
379
+ // so a scan spanning sibling projects applies the right rules per file.
380
+ const fileOptions = scanOptionsFor(file);
247
381
  let fileFindings;
248
382
  if (HTML_EXTENSIONS.has(ext)) {
249
- fileFindings = await detectHtml(file, scanOptions);
383
+ fileFindings = await detectHtml(file, fileOptions);
250
384
  } else {
251
- fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions);
385
+ fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, fileOptions);
252
386
  }
253
387
  // Annotate findings with import context
254
388
  const importers = importedByMap.get(file);
@@ -263,10 +397,11 @@ async function detectCli() {
263
397
  } else if (stat.isFile()) {
264
398
  if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
265
399
  const ext = path.extname(resolved).toLowerCase();
400
+ const fileOptions = scanOptionsFor(resolved);
266
401
  if (HTML_EXTENSIONS.has(ext)) {
267
- allFindings.push(...await detectHtml(resolved, scanOptions));
402
+ allFindings.push(...await detectHtml(resolved, fileOptions));
268
403
  } else {
269
- allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions));
404
+ allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, fileOptions));
270
405
  }
271
406
  }
272
407
  }
@@ -276,12 +411,25 @@ async function detectCli() {
276
411
  }
277
412
 
278
413
  allFindings = filterDetectionFindings(allFindings, detectionConfig);
414
+ allFindings = filterByScopes(allFindings, scopes);
415
+ // --no-advisory drops advisory findings before any output or exit-code math.
416
+ if (noAdvisory) allFindings = allFindings.filter((f) => !isAdvisory(f));
417
+
418
+ // The exit code and failure count reflect non-advisory findings only. An
419
+ // advisory-only scan still prints its notes but exits 0 (a clean pass), so
420
+ // advisory rules never break CI or block automation.
421
+ const { primary, advisory } = partitionAdvisory(allFindings);
279
422
 
280
423
  if (allFindings.length > 0) {
281
424
  if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');
282
- else if (quietMode) process.stderr.write(formatFindingSummary(allFindings.length) + '\n');
425
+ else if (quietMode) {
426
+ process.stderr.write(formatFindingSummary(primary.length) + '\n');
427
+ if (advisory.length > 0) {
428
+ process.stderr.write(dim(`${advisory.length} advisory note${advisory.length === 1 ? '' : 's'} (not counted).`) + '\n');
429
+ }
430
+ }
283
431
  else process.stderr.write(formatFindings(allFindings, false) + '\n');
284
- process.exit(2);
432
+ process.exit(primary.length > 0 ? 2 : 0);
285
433
  }
286
434
  if (jsonMode) process.stdout.write('[]\n');
287
435
  process.exit(0);