@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  8. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  9. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  10. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  11. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  12. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  13. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  14. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  15. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  16. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  17. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  18. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  19. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  20. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  21. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  22. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  24. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  30. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  31. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  32. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  34. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  35. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  37. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  39. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  40. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  41. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  44. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  66. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  67. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  68. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  70. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  100. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  101. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  102. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  103. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  104. package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
  105. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  106. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  107. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  108. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  109. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  110. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  111. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
  112. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  113. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  115. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  116. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  117. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  118. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  120. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  121. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  123. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  124. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  125. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  127. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  128. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  129. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  131. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  132. package/dist/modes/interactive/interactive-agent-events.js +9 -3
  133. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  134. package/docs/tui.md +7 -2
  135. package/docs/workflows.md +1 -1
  136. package/npm-shrinkwrap.json +23 -23
  137. package/package.json +2 -2
  138. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  139. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  140. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
@@ -3,9 +3,68 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { finding } from '../../findings.mjs';
6
- import { filterByProviders } from '../../registry/antipatterns.mjs';
7
6
  import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profile/profiler.mjs';
8
7
  import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
8
+ import { checkContentHiddenAtRest } from '../../rules/checks.mjs';
9
+
10
+ // On Windows, puppeteer's bundled Chrome lives in a user-writable cache
11
+ // directory. Its GPU process can be denied (STATUS_ACCESS_DENIED) by security
12
+ // software or the GPU sandbox because it launches from an untrusted path.
13
+ // Chrome then crash-loops the GPU process, and each relaunch briefly flashes a
14
+ // compositor surface, the black window users report during `detect <url>`
15
+ // (issue #372). The system-installed Chrome runs from a trusted location with a
16
+ // healthy GPU, so channel:'chrome' avoids the crash entirely; both use hardware
17
+ // GPU, so contrast measurement is unaffected. Scope this to Windows only: other
18
+ // platforms do not have the bug, so they keep the pinned bundled build for
19
+ // consistent measurement across machines. Fall back to bundled when the switch
20
+ // fails (Chrome not installed, or channel resolution fails). If the bundled
21
+ // launch then also fails, surface the original system-Chrome error as the
22
+ // cause so the real failure is not lost.
23
+ async function launchBrowser(puppeteer, { headless = true, args = [] } = {}) {
24
+ let channelError;
25
+ if (process.platform === 'win32') {
26
+ try {
27
+ return await puppeteer.default.launch({ channel: 'chrome', headless, args });
28
+ } catch (err) {
29
+ // System Chrome unavailable or unlaunchable; fall through to the bundled
30
+ // browser, but keep the error in case the fallback fails too.
31
+ channelError = err;
32
+ }
33
+ }
34
+ try {
35
+ return await puppeteer.default.launch({ headless, args });
36
+ } catch (err) {
37
+ if (channelError && err && err.cause === undefined) err.cause = channelError;
38
+ throw err;
39
+ }
40
+ }
41
+
42
+ // Reveal sweep + invisible-text measurement for the content-hidden-at-rest
43
+ // rule. Scrolls through the document with instant jumps (bypasses CSS
44
+ // scroll-behavior: smooth) so IntersectionObserver / scroll reveal handlers
45
+ // get every chance to fire, returns to the top, lets transitions settle,
46
+ // then measures how much text still renders invisible. A healthy
47
+ // reveal-on-scroll page drops to ~0 after the sweep; a page whose reveal
48
+ // script died keeps most of its text at opacity 0.
49
+ async function measureContentHiddenAfterReveal(page) {
50
+ await page.evaluate(async () => {
51
+ const step = Math.max(200, Math.floor(window.innerHeight * 0.7));
52
+ const max = Math.max(
53
+ document.documentElement.scrollHeight || 0,
54
+ document.body?.scrollHeight || 0,
55
+ );
56
+ for (let y = 0; y <= max; y += step) {
57
+ window.scrollTo({ top: y, left: 0, behavior: 'instant' });
58
+ await new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 40)));
59
+ }
60
+ window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
61
+ await new Promise(resolve => setTimeout(resolve, 700));
62
+ });
63
+ return page.evaluate(() => {
64
+ if (typeof window.impeccableMeasureHiddenText !== 'function') return null;
65
+ return window.impeccableMeasureHiddenText();
66
+ });
67
+ }
9
68
 
10
69
  function serializeDesignSystemForBrowser(designSystem) {
11
70
  if (!designSystem?.present) return null;
@@ -151,13 +210,26 @@ async function detectUrl(url, options = {}) {
151
210
  phase: 'load',
152
211
  ruleId: 'launch-browser',
153
212
  target: url,
154
- }, () => puppeteer.default.launch({ headless: true, args: launchArgs }));
213
+ }, () => launchBrowser(puppeteer, { headless: options?.headless ?? true, args: launchArgs }));
155
214
  const page = await profileStepAsync(profile, {
156
215
  engine: 'browser',
157
216
  phase: 'load',
158
217
  ruleId: 'new-page',
159
218
  target: url,
160
219
  }, () => browser.newPage());
220
+
221
+ // Uncaught exceptions and parse errors surface as pageerror events. The
222
+ // listener must attach before goto: a syntax error fires during the
223
+ // initial parse, long before the load event. Dedupe by message; a single
224
+ // broken loop can otherwise throw hundreds of identical errors.
225
+ const pageErrors = [];
226
+ if (options?.scriptErrors !== false) {
227
+ page.on('pageerror', (err) => {
228
+ const message = String(err?.message || err).split('\n')[0].trim().slice(0, 160);
229
+ if (message && !pageErrors.includes(message)) pageErrors.push(message);
230
+ });
231
+ }
232
+
161
233
  let results = [];
162
234
  try {
163
235
  await profileStepAsync(profile, {
@@ -213,9 +285,29 @@ async function detectUrl(url, options = {}) {
213
285
  return window.impeccableDetect({ decorate: false, serialize: true });
214
286
  });
215
287
  return serializedGroups.flatMap(({ findings }) =>
216
- findings.map(f => ({ id: f.type, snippet: f.detail, ignoreValue: f.ignoreValue || '' }))
288
+ findings.map(f => ({ id: f.type, snippet: f.detail, ignoreValue: f.ignoreValue || '', severity: f.severity || '' }))
217
289
  );
218
290
  });
291
+ // Content invisible at rest: reveal sweep, then re-measure. Runs after
292
+ // the main scan (which must see the true at-rest state) and before the
293
+ // visual contrast fallback (the sweep restores scroll to the top).
294
+ if (options?.contentHidden !== false) {
295
+ const hiddenFindings = await profileFindingsAsync(profile, {
296
+ engine: 'browser',
297
+ phase: 'scan',
298
+ ruleId: 'content-hidden-at-rest',
299
+ target: url,
300
+ }, async () => {
301
+ const measured = await measureContentHiddenAfterReveal(page);
302
+ return measured ? checkContentHiddenAtRest(measured) : [];
303
+ });
304
+ results.push(...hiddenFindings);
305
+ }
306
+
307
+ for (const message of pageErrors.slice(0, 3)) {
308
+ results.push({ id: 'script-error', snippet: message });
309
+ }
310
+
219
311
  const visualFindings = await runVisualContrastFallback(page, serializedGroups, options, profile, url);
220
312
  results.push(...visualFindings);
221
313
  } finally {
@@ -234,11 +326,14 @@ async function detectUrl(url, options = {}) {
234
326
  }, () => browser.close());
235
327
  }
236
328
  }
237
- return filterByProviders(results.map(f => {
329
+ return results.map(f => {
238
330
  const item = finding(f.id, url, f.snippet);
239
331
  if (f.ignoreValue) item.ignoreValue = f.ignoreValue;
332
+ // Per-finding severity promotion (e.g. hero-region pulsing dot)
333
+ // overrides the registry default carried by finding().
334
+ if (f.severity && f.severity !== item.severity) item.severity = f.severity;
240
335
  return item;
241
- }), options.providers);
336
+ });
242
337
  }
243
338
 
244
339
  async function createBrowserDetector(options = {}) {
@@ -249,7 +344,7 @@ async function createBrowserDetector(options = {}) {
249
344
  throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
250
345
  }
251
346
  const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
252
- const browser = options.browser || await puppeteer.default.launch({
347
+ const browser = options.browser || await launchBrowser(puppeteer, {
253
348
  headless: options.headless ?? true,
254
349
  args: launchArgs,
255
350
  });
@@ -274,4 +369,4 @@ async function createBrowserDetector(options = {}) {
274
369
  };
275
370
  }
276
371
 
277
- export { runVisualContrastFallback, detectUrl, createBrowserDetector };
372
+ export { runVisualContrastFallback, detectUrl, createBrowserDetector, launchBrowser };
@@ -1,11 +1,11 @@
1
- import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
1
+ import { GENERIC_FONTS, OVERUSED_FONTS, EM_DASH_FLOOR, EM_DASH_CHARS_PER_DASH } from '../../shared/constants.mjs';
2
2
  import { isNeutralColor } from '../../shared/color.mjs';
3
3
  import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
4
4
  import { checkSourceDesignSystem } from '../../design-system.mjs';
5
+ import { scanCssTextForGlow, scanCssTextForGridBackground, scanCssTextForMarquee, scanCssTextForPseudoStripe, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
5
6
  import { isFullPage, stripHtmlBlocks } from '../../shared/page.mjs';
6
7
  import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
7
8
  import { finding } from '../../findings.mjs';
8
- import { filterByProviders } from '../../registry/antipatterns.mjs';
9
9
  import { profileFindings, profileStep } from '../../profile/profiler.mjs';
10
10
 
11
11
  // ---------------------------------------------------------------------------
@@ -16,6 +16,7 @@ const hasRounded = (line) => /\brounded(?:-\w+)?\b/.test(line);
16
16
  const hasBorderRadius = (line) => /border-radius/i.test(line);
17
17
  const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|input[\s>]|span[\s>])/i.test(line);
18
18
 
19
+
19
20
  /** Strip HTML to plain text — drops script/style/comments/tags so
20
21
  * content-text analyzers don't false-positive on code or CSS. */
21
22
  function stripHtmlToText(html) {
@@ -40,25 +41,96 @@ function firstOverusedGoogleFont(text) {
40
41
  return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
41
42
  }
42
43
 
43
- function isNeutralBorderColor(str) {
44
- const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
45
- if (!m) return false;
46
- const c = m[1].toLowerCase();
47
- if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true;
48
- if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
49
- const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
50
- if (hex) {
51
- const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)];
52
- return (Math.max(r, g, b) - Math.min(r, g, b)) < 30;
44
+ // CSS named colors whose channels are equal (achromatic). Anything outside
45
+ // this set falls through to the format parsers, and an unrecognized spelling
46
+ // stays non-neutral so a real accent is never skipped.
47
+ const NEUTRAL_COLOR_KEYWORDS = new Set([
48
+ 'transparent', 'currentcolor',
49
+ 'black', 'white', 'gray', 'grey', 'silver',
50
+ 'dimgray', 'dimgrey', 'darkgray', 'darkgrey', 'lightgray', 'lightgrey',
51
+ 'gainsboro', 'whitesmoke',
52
+ ]);
53
+
54
+ function hexChannels(color) {
55
+ const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
56
+ if (long) return [parseInt(long[1], 16), parseInt(long[2], 16), parseInt(long[3], 16)];
57
+ const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
58
+ if (short) return [1, 2, 3].map((i) => parseInt(short[i] + short[i], 16));
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Split one box-shadow layer into top-level tokens.
64
+ *
65
+ * Whitespace inside parens does not separate tokens: `rgb(0 0 0)` and
66
+ * `var(--x, 4px)` are each a single value, and splitting them on spaces would
67
+ * read their innards as separate lengths.
68
+ */
69
+ function tokenizeShadowLayer(layer) {
70
+ const tokens = [];
71
+ let depth = 0;
72
+ let current = '';
73
+ for (const char of String(layer || '')) {
74
+ if (char === '(') depth++;
75
+ else if (char === ')') depth--;
76
+ else if (depth === 0 && /\s/.test(char)) {
77
+ if (current) tokens.push(current);
78
+ current = '';
79
+ continue;
80
+ }
81
+ current += char;
53
82
  }
54
- const shex = c.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
55
- if (shex) {
56
- const [r, g, b] = [parseInt(shex[1] + shex[1], 16), parseInt(shex[2] + shex[2], 16), parseInt(shex[3] + shex[3], 16)];
57
- return (Math.max(r, g, b) - Math.min(r, g, b)) < 30;
83
+ if (current) tokens.push(current);
84
+ return tokens;
85
+ }
86
+
87
+ function lastMatch(text, re) {
88
+ const all = [...String(text || '').matchAll(re)];
89
+ return all.length ? all[all.length - 1] : null;
90
+ }
91
+
92
+ function isShadowLength(token) {
93
+ return /^-?\d*\.?\d+(?:px)?$/i.test(String(token || ''));
94
+ }
95
+
96
+ /**
97
+ * Neutrality test for colors as written in source CSS.
98
+ *
99
+ * shared/color.mjs's isNeutralColor only parses the computed function forms a
100
+ * browser or jsdom emits (rgb/oklch/lab/...) and deliberately reports every
101
+ * other spelling as chromatic so an unknown format is never silently skipped.
102
+ * That default is wrong for authored CSS, where `#000` and `black` are the
103
+ * normal spellings: calling it directly reports a plain black hairline as a
104
+ * colored stripe. Handle hex and named neutrals here, then defer.
105
+ */
106
+ function isNeutralAuthoredColor(rawColor) {
107
+ const c = String(rawColor || '').trim().toLowerCase();
108
+ if (!c) return false;
109
+ if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
110
+ // Modern rgb() takes space-separated channels (`rgb(0 0 0)`). shared/color.mjs
111
+ // parses only the comma form a browser's getComputedStyle emits, so authored
112
+ // space-separated neutrals fell through it and reported as chromatic — the
113
+ // exemption this function exists for, missed. Normalize before delegating.
114
+ if (/^rgba?\(/i.test(c)) {
115
+ const channels = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
116
+ if (channels) {
117
+ const values = [1, 2, 3].map((i) => Number(channels[i]));
118
+ return (Math.max(...values) - Math.min(...values)) < 30;
119
+ }
120
+ return isNeutralColor(c);
58
121
  }
122
+ if (/^(?:hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
123
+ const channels = hexChannels(c);
124
+ if (channels) return (Math.max(...channels) - Math.min(...channels)) < 30;
59
125
  return false;
60
126
  }
61
127
 
128
+ function isNeutralBorderColor(str) {
129
+ const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
130
+ if (!m) return false;
131
+ return isNeutralAuthoredColor(m[1]);
132
+ }
133
+
62
134
  const REGEX_MATCHERS = [
63
135
  // --- Side-tab ---
64
136
  { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g,
@@ -232,15 +304,34 @@ const REGEX_ANALYZERS = [
232
304
  const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
233
305
  return [finding('monotonous-spacing', filePath, `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(pct * 100)}%)`)];
234
306
  },
235
- // Em-dash overuse: 5+ em-dashes or "--" in body text content
236
- // (occasional em-dash use in prose is fine; the pattern fires only
237
- // when count crosses into AI-cadence territory).
307
+ // Em-dash overuse (ADVISORY): the AI cadence tell is em-dash *saturation*,
308
+ // not the occasional dash. Humans use em-dashes legitimately, so this rule is
309
+ // advisory (surfaced separately, never a failure, hook-skipped by default) and
310
+ // its threshold is deliberately conservative. Two gates must both hold:
311
+ // 1. Absolute floor of EM_DASH_FLOOR (8) dashes — a page with a handful
312
+ // never fires, no matter how short.
313
+ // 2. Density: at least one dash per EM_DASH_CHARS_PER_DASH (500) characters
314
+ // of body text, so a long article that uses eight across several thousand
315
+ // words is left alone while a short, dash-per-clause landing page is not.
316
+ // Raised from the old flat 5-dash floor, which fired on ordinary long prose.
317
+ //
318
+ // stripHtmlToText drops tags but leaves character-entity escapes intact, so
319
+ // a model that writes `&mdash;`, `&#8212;`, or `&#x2014;` renders an em-dash
320
+ // the counter never saw. Decode the em-dash entities (named, zero-padded
321
+ // decimal, upper/lower hex) to the literal glyph first. En-dash entities are
322
+ // deliberately left alone: the rule counts em-dashes, and the literal `–`
323
+ // was never counted either.
238
324
  (content, filePath) => {
239
- const text = stripHtmlToText(content);
325
+ const text = stripHtmlToText(content)
326
+ .replace(/&mdash;|&#0*8212;|&#x0*2014;/gi, '—');
240
327
  let count = 0;
241
328
  const re = /[—]|--(?=\S)/g;
242
329
  while (re.exec(text) !== null) count++;
243
- if (count < 5) return [];
330
+ if (count < EM_DASH_FLOOR) return [];
331
+ // Saturation gate: dashes must be dense in the prose, not sprinkled through
332
+ // a long document. textLength <= count * chars-per-dash means the density is
333
+ // at or above the threshold.
334
+ if (text.length > count * EM_DASH_CHARS_PER_DASH) return [];
244
335
  return [finding('em-dash-overuse', filePath, `${count} em-dashes in body text`)];
245
336
  },
246
337
  // Marketing buzzwords: SaaS phrase list
@@ -276,22 +367,6 @@ const REGEX_ANALYZERS = [
276
367
  if (count === 0) return [];
277
368
  return [finding('marketing-buzzword', filePath, `${count} buzzword phrase${count === 1 ? '' : 's'}: "${firstSample}"`)];
278
369
  },
279
- // Numbered section markers (01 / 02 / 03 ...)
280
- (content, filePath) => {
281
- const text = stripHtmlToText(content);
282
- const re = /\b(0[1-9]|1[0-2])\b/g;
283
- const seen = new Set();
284
- let m;
285
- while ((m = re.exec(text)) !== null) seen.add(m[1]);
286
- if (seen.size < 3) return [];
287
- const sorted = [...seen].sort();
288
- let sequential = 0;
289
- for (let i = 1; i < sorted.length; i++) {
290
- if (parseInt(sorted[i], 10) === parseInt(sorted[i - 1], 10) + 1) sequential++;
291
- }
292
- if (sequential < 2) return [];
293
- return [finding('numbered-section-markers', filePath, `Sequence: ${sorted.slice(0, 6).join(', ')}`)];
294
- },
295
370
  // Aphoristic cadence: manufactured-contrast + short-rebuttal
296
371
  (content, filePath) => {
297
372
  const text = stripHtmlToText(content);
@@ -313,41 +388,143 @@ const REGEX_ANALYZERS = [
313
388
  if (count < 3) return [];
314
389
  return [finding('aphoristic-cadence', filePath, `${count} aphoristic constructions: "${firstSample}"`)];
315
390
  },
316
- // Dark glow (page-level: dark bg + colored box-shadow with blur)
391
+ // Dark glow / chromatic halo shadows (page-level). Shared scanner handles
392
+ // any color format, single-level var() resolution, zero-offset halos on
393
+ // any background, and text-shadow glows.
317
394
  (content, filePath) => {
318
- // Check if page has a dark background
319
- const darkBgRe = /background(?:-color)?\s*:\s*(?:#(?:0[0-9a-f]|1[0-9a-f]|2[0-3])[0-9a-f]{4}\b|#(?:0|1)[0-9a-f]{2}\b|rgb\(\s*(\d{1,2})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\))/gi;
320
- const twDarkBg = /\bbg-(?:gray|slate|zinc|neutral|stone)-(?:9\d{2}|800)\b/;
321
- const hasDarkBg = darkBgRe.test(content) || twDarkBg.test(content);
322
- if (!hasDarkBg) return [];
323
-
324
- // Check for colored box-shadow with blur > 4px
325
- const shadowRe = /box-shadow\s*:\s*([^;{}]+)/gi;
326
- let m;
327
- while ((m = shadowRe.exec(content)) !== null) {
328
- const val = m[1];
329
- const colorMatch = val.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
330
- if (!colorMatch) continue;
331
- const [r, g, b] = [+colorMatch[1], +colorMatch[2], +colorMatch[3]];
332
- if ((Math.max(r, g, b) - Math.min(r, g, b)) < 30) continue; // skip gray
333
- // Check blur: look for pattern like "0 0 20px" (third number > 4)
334
- const pxVals = [...val.matchAll(/(\d+)px|(?<![.\d])\b(0)\b(?![.\d])/g)].map(p => +(p[1] || p[2]));
335
- if (pxVals.length >= 3 && pxVals[2] > 4) {
336
- const lines = content.substring(0, m.index).split('\n');
337
- return [finding('dark-glow', filePath, `Colored glow (rgb(${r},${g},${b})) on dark page`, lines.length)];
338
- }
339
- }
340
- return [];
395
+ const hits = scanCssTextForGlow(content);
396
+ if (hits.length === 0) return [];
397
+ const lines = content.substring(0, hits[0].index).split('\n');
398
+ return [finding('dark-glow', filePath, hits[0].snippet, lines.length)];
341
399
  },
400
+ // Radial-gradient background halo on a dark page (the gradient sibling
401
+ // of the dark-glow shadow tell).
402
+ (content, filePath) => {
403
+ const hits = scanCssTextForRadialHalo(content);
404
+ if (hits.length === 0) return [];
405
+ const lines = content.substring(0, hits[0].index).split('\n');
406
+ return [finding('radial-halo', filePath, hits[0].snippet, lines.length)];
407
+ },
408
+ // Auto-scrolling marquees (<marquee> or infinite horizontal loop
409
+ // animations).
410
+ (content, filePath) => scanCssTextForMarquee(content).map(hit => finding('marquee', filePath, hit.snippet)),
342
411
  ];
343
412
 
344
413
  // ---------------------------------------------------------------------------
345
- // Style block extraction (Vue/Svelte <style> blocks)
414
+ // Structural CSS checks used by source files whose styles are not parsed by
415
+ // the static HTML engine.
416
+ // ---------------------------------------------------------------------------
417
+
418
+ const CHROMATIC_SHADOW_TOKEN_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
419
+
420
+ function insetStripeColorIsChromatic(rawColor) {
421
+ const color = String(rawColor || '').trim().replace(/\s*!important\s*$/i, '');
422
+ if (/^(?:currentcolor|transparent|inherit|unset)$/i.test(color)) return false;
423
+ const variable = color.match(/^var\(\s*(--[\w-]+)/i);
424
+ if (variable) return CHROMATIC_SHADOW_TOKEN_RE.test(variable[1]);
425
+ if (!/^(?:#|rgba?\(|hsla?\(|hwb\(|oklch\(|oklab\(|lch\(|lab\(|color\(|[a-z]+$)/i.test(color)) return false;
426
+ return !isNeutralAuthoredColor(color);
427
+ }
428
+
429
+ /**
430
+ * Blank out comment bodies while preserving every byte offset (and therefore
431
+ * every line number) so commented-out CSS is not scanned as live rules.
432
+ */
433
+ function blankCssComments(css) {
434
+ return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '));
435
+ }
436
+
437
+ function scanInsetStripeCss(rawContent, filePath, lineOffset = 0) {
438
+ const content = blankCssComments(rawContent);
439
+ const findings = [];
440
+ const ruleRe = /([^{};]+)\{([^{}]*)\}/g;
441
+ let match;
442
+ // Deriving each line with content.slice(0, offset).split('\n') re-scans the
443
+ // whole prefix per rule, which is O(n^2) on a large stylesheet. Rule matches
444
+ // arrive in source order, so carry a monotonic cursor instead: one pass total.
445
+ let scanOffset = 0;
446
+ let scanLine = 1;
447
+ const lineAtOffset = (offset) => {
448
+ while (scanOffset < offset) {
449
+ if (content[scanOffset] === '\n') scanLine++;
450
+ scanOffset++;
451
+ }
452
+ return scanLine;
453
+ };
454
+ while ((match = ruleRe.exec(content)) !== null) {
455
+ // The selector group is `[^{};]+`, which greedily absorbs the whitespace and
456
+ // newlines trailing the previous rule. Advance past that run before deriving
457
+ // the line, or every rule after the first reports the preceding line.
458
+ const selectorStart = match.index + (match[1].length - match[1].trimStart().length);
459
+ const selector = match[1].trim().replace(/\s+/g, ' ');
460
+ if (!selector) continue;
461
+ if (/:(?:hover|focus|focus-visible|focus-within|active|checked|target)\b/i.test(selector)) continue;
462
+ if (/\[aria-selected\s*[*^$|~]?=\s*["']?true/i.test(selector)) continue;
463
+ if (/\[aria-current(?!\s*[*^$|~]?=\s*["']?false)/i.test(selector)) continue;
464
+ if (/(?:^|[\s._[-])(?:active|current|selected)(?![\w])/i.test(selector)) continue;
465
+ if (/(?:^|[\s>+~,(])(?:button|hr|tr|td|th|table|blockquote|pre|code)(?![\w-])/i.test(selector)) continue;
466
+
467
+ // Read the last of a repeated declaration, not the first: that is what the
468
+ // cascade paints. Taking the first both flagged stripes that a later
469
+ // `box-shadow: none` had cancelled and missed stripes that overrode an
470
+ // earlier value, and mis-skipped rules whose narrow width was overridden.
471
+ const width = lastMatch(match[2], /(?:^|;)\s*(?:width|inline-size)\s*:\s*(\d+(?:\.\d+)?)px/gi);
472
+ if (width && Number(width[1]) <= 40) continue;
473
+ const declaration = lastMatch(match[2], /(?:^|;)\s*box-shadow\s*:\s*([^;]+)/gi);
474
+ if (!declaration || !/\binset\b/i.test(declaration[1])) continue;
475
+ // `!important` qualifies the declaration, not the shadow value, so strip it
476
+ // before the layers are read. Tokenizing split it into its own token, which
477
+ // made the color count wrong and silently stopped flagging stripes declared
478
+ // with it — a shape the previous regex handled.
479
+ const shadowValue = declaration[1].replace(/\s*!\s*important\s*$/i, '').trim();
480
+
481
+ for (const rawLayer of shadowValue.split(/,(?![^(]*\))/)) {
482
+ const layer = rawLayer.trim();
483
+ // Parse the layer by its grammar rather than by one spelling of it.
484
+ // A box-shadow layer is `inset? && <length>{2,4} && <color>?` in any
485
+ // order, so `inset 4px 0 red`, `4px 0 0 red inset`, and `red 4px 0 inset`
486
+ // all paint the same stripe. Matching a fixed token order missed three
487
+ // valid spellings in a row; enumerate the tokens instead. Tokenizing must
488
+ // respect parens: `rgb(0 0 0)` is one color token, and splitting it on
489
+ // whitespace would read its channels as lengths.
490
+ const tokens = tokenizeShadowLayer(layer);
491
+ if (!tokens.some((token) => /^inset$/i.test(token))) continue;
492
+ const rest = tokens.filter((token) => !/^inset$/i.test(token));
493
+ const lengths = rest.filter(isShadowLength);
494
+ const colors = rest.filter((token) => !isShadowLength(token));
495
+ // Only the two offsets are required; omitted blur/spread default to 0,
496
+ // which is exactly the stripe shape. More than one non-length token is a
497
+ // layer shape we do not claim to understand, so leave it alone.
498
+ if (lengths.length < 2 || lengths.length > 4 || colors.length !== 1) continue;
499
+ const values = lengths.map((token) => ({
500
+ n: Number(token.replace(/px$/i, '')),
501
+ hasPx: /px$/i.test(token),
502
+ }));
503
+ const x = values[0];
504
+ const y = values[1];
505
+ const blur = values[2] ? values[2].n : 0;
506
+ const spread = values[3] ? values[3].n : 0;
507
+ if ((x.n !== 0 && !x.hasPx) || (y.n !== 0 && !y.hasPx) || blur !== 0 || spread !== 0) continue;
508
+ const ax = Math.abs(x.n);
509
+ const ay = Math.abs(y.n);
510
+ if (!((ax >= 3 && ax <= 12 && ay === 0) || (ay >= 3 && ay <= 12 && ax === 0))) continue;
511
+ if (!insetStripeColorIsChromatic(colors[0])) continue;
512
+ const edge = ay === 0 ? (x.n > 0 ? 'left' : 'right') : (y.n > 0 ? 'top' : 'bottom');
513
+ const line = lineOffset + lineAtOffset(selectorStart);
514
+ findings.push(finding('side-tab', filePath, `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`, line));
515
+ break;
516
+ }
517
+ }
518
+ return findings;
519
+ }
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Style block extraction (Astro/Vue/Svelte <style> blocks)
346
523
  // ---------------------------------------------------------------------------
347
524
 
348
525
  function extractStyleBlocks(content, ext) {
349
526
  ext = ext.toLowerCase();
350
- if (ext !== '.vue' && ext !== '.svelte') return [];
527
+ if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
351
528
  const blocks = [];
352
529
  const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
353
530
  let m;
@@ -432,21 +609,20 @@ function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null,
432
609
  }
433
610
 
434
611
  /** Page-level analyzers that scan rendered text content (em-dash use,
435
- * buzzword phrases, numbered section markers, aphoristic cadence).
612
+ * buzzword phrases, aphoristic cadence).
436
613
  * These are detector-agnostic — they work on any HTML/text source
437
614
  * and don't need a parsed DOM. Exported so detectHtml can call them
438
615
  * for `.html` files (which otherwise skip the regex engine). */
439
616
  const TEXT_CONTENT_ANALYZER_IDS = [
440
617
  'em-dash-overuse',
441
618
  'marketing-buzzword',
442
- 'numbered-section-markers',
443
619
  'aphoristic-cadence',
444
620
  ];
445
621
 
446
622
  function runTextContentAnalyzers(content, filePath, options = {}) {
447
623
  const profile = options?.profile;
448
624
  if (!shouldRunPageAnalyzers(content, filePath)) return [];
449
- // The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
625
+ // The 3 text-content analyzers are at indices 3-5 in REGEX_ANALYZERS.
450
626
  const findings = [];
451
627
  for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
452
628
  const analyzer = REGEX_ANALYZERS[3 + i];
@@ -474,8 +650,36 @@ function detectText(content, filePath, options = {}) {
474
650
  profile,
475
651
  phase: 'source',
476
652
  }));
653
+ // Pseudo-element stripes (::before/::after absolute bars) carry the same
654
+ // side-tab silhouette without any border token, so the line matchers can't
655
+ // see them (issue #394). The shared scanner already runs on full HTML pages
656
+ // via checkHtmlPatterns; give standalone stylesheets, component style
657
+ // blocks, and CSS-in-JS templates the same coverage. Each hit carries the
658
+ // rule's source offset, so the finding gets a real line and line-scoped
659
+ // inline ignores keep working.
660
+ const pseudoStripeFindings = (text, lineOffset) =>
661
+ scanCssTextForPseudoStripe(text).map(hit =>
662
+ finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
663
+
664
+ if (cssLike.has(ext)) {
665
+ findings.push(...scanInsetStripeCss(content, filePath));
666
+ findings.push(...pseudoStripeFindings(content, 0));
667
+ }
668
+
669
+ // Block-level CSS checks that need multiple declarations must run over the
670
+ // complete source, not line-by-line. This covers standalone stylesheets,
671
+ // component style blocks, inline styles, and CSS-in-JS templates.
672
+ findings.push(...profileFindings(profile, {
673
+ engine: 'regex',
674
+ phase: 'source',
675
+ ruleId: 'codex-grid-background',
676
+ target: filePath,
677
+ }, () => scanCssTextForGridBackground(content).map(hit => {
678
+ const line = content.substring(0, hit.index).split('\n').length;
679
+ return finding('codex-grid-background', filePath, hit.snippet, line);
680
+ })));
477
681
 
478
- // Extract and scan <style> blocks from Vue/Svelte SFCs
682
+ // Extract and scan <style> blocks from Astro/Vue/Svelte components.
479
683
  const styleBlocks = profile
480
684
  ? profileStep(profile, {
481
685
  engine: 'regex',
@@ -490,6 +694,14 @@ function detectText(content, filePath, options = {}) {
490
694
  profile,
491
695
  phase: 'style-block',
492
696
  }));
697
+ // block.startLine is the first line *after* the <style> tag, but block.content
698
+ // begins at the character right after that tag — so its own line 1 sits on the
699
+ // tag's line, whether or not a newline follows immediately. lineAtOffset is
700
+ // 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
701
+ // reported every selector one line low. runRegexMatchers keeps startLine - 1
702
+ // because it indexes its split lines from zero.
703
+ findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
704
+ findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
493
705
  }
494
706
 
495
707
  // Extract and scan CSS-in-JS template literals
@@ -507,6 +719,8 @@ function detectText(content, filePath, options = {}) {
507
719
  profile,
508
720
  phase: 'css-in-js',
509
721
  }));
722
+ findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 1));
723
+ findings.push(...pseudoStripeFindings(block.content, block.startLine - 1));
510
724
  }
511
725
 
512
726
  if (options?.designSystem) {
@@ -537,7 +751,6 @@ function detectText(content, filePath, options = {}) {
537
751
  'monotonous-spacing',
538
752
  'em-dash-overuse',
539
753
  'marketing-buzzword',
540
- 'numbered-section-markers',
541
754
  'aphoristic-cadence',
542
755
  'dark-glow',
543
756
  ];
@@ -552,10 +765,9 @@ function detectText(content, filePath, options = {}) {
552
765
  }
553
766
  }
554
767
 
555
- const byProvider = filterByProviders(deduped, options?.providers);
556
768
  // Inline `impeccable-disable*` waivers travel with the file; honor them unless
557
769
  // explicitly bypassed (`--no-config` / `--no-inline-ignores`).
558
- return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, content);
770
+ return options?.inlineIgnores === false ? deduped : applyInlineIgnores(deduped, content);
559
771
  }
560
772
 
561
773
  export {