@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.
- package/CHANGELOG.md +14 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +22 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +9 -3
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/docs/tui.md +7 -2
- package/docs/workflows.md +1 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
|
@@ -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
|
-
|
|
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 [
|
|
194
|
-
|
|
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
|
}
|
package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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 =>
|
|
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,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { loadDesignSystemForTarget } from '../design-system.mjs';
|
|
5
6
|
import { RULE_SCOPES, filterByScopes } from '../registry/antipatterns.mjs';
|
|
6
7
|
import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs';
|
|
7
8
|
import { detectHtml } from '../engines/static-html/detect-html.mjs';
|
|
@@ -27,9 +28,37 @@ function formatFindingSummary(count) {
|
|
|
27
28
|
return `${count} anti-pattern${count === 1 ? '' : 's'} found.`;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
60
|
|
|
61
|
+
function formatFindingsBody(findings) {
|
|
33
62
|
const grouped = {};
|
|
34
63
|
for (const f of findings) {
|
|
35
64
|
if (!grouped[f.file]) grouped[f.file] = [];
|
|
@@ -44,7 +73,28 @@ function formatFindings(findings, jsonMode) {
|
|
|
44
73
|
out.push(` → ${item.description}`);
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
|
-
out
|
|
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);
|
|
48
98
|
return out.join('\n');
|
|
49
99
|
}
|
|
50
100
|
|
|
@@ -52,7 +102,11 @@ function formatFindings(findings, jsonMode) {
|
|
|
52
102
|
// Stdin handling
|
|
53
103
|
// ---------------------------------------------------------------------------
|
|
54
104
|
|
|
55
|
-
|
|
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;
|
|
56
110
|
const chunks = [];
|
|
57
111
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
58
112
|
const input = Buffer.concat(chunks).toString('utf-8');
|
|
@@ -60,11 +114,12 @@ async function handleStdin(options = {}) {
|
|
|
60
114
|
const parsed = JSON.parse(input);
|
|
61
115
|
const fp = parsed?.tool_input?.file_path;
|
|
62
116
|
if (fp && fs.existsSync(fp)) {
|
|
117
|
+
const options = resolve(fp);
|
|
63
118
|
return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())
|
|
64
119
|
? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);
|
|
65
120
|
}
|
|
66
121
|
} catch { /* not JSON */ }
|
|
67
|
-
return detectText(input, '<stdin>',
|
|
122
|
+
return detectText(input, '<stdin>', resolve(null));
|
|
68
123
|
}
|
|
69
124
|
|
|
70
125
|
|
|
@@ -92,16 +147,22 @@ Scan files or URLs for UI anti-patterns and design quality issues.
|
|
|
92
147
|
Options:
|
|
93
148
|
--json Output results as JSON
|
|
94
149
|
--quiet In text mode, only print the final findings count
|
|
95
|
-
--gpt Also report GPT-specific provider tells (off by default)
|
|
96
|
-
--gemini Also report Gemini-specific provider tells (off by default)
|
|
97
150
|
--scope <name> Only report rules in the given design domain
|
|
98
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
|
|
99
154
|
--no-config Do not apply project config, detector ignores, inline
|
|
100
155
|
ignore comments, or DESIGN.md
|
|
101
156
|
--no-inline-ignores Do not honor in-file impeccable-disable* ignore comments
|
|
102
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)
|
|
103
159
|
--help Show this help message
|
|
104
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
|
+
|
|
105
166
|
Project config:
|
|
106
167
|
Respects .impeccable/config.json and .impeccable/config.local.json detector
|
|
107
168
|
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
|
|
@@ -118,7 +179,8 @@ Inline ignores:
|
|
|
118
179
|
Detection modes:
|
|
119
180
|
HTML files Static HTML/CSS analysis (default, catches linked CSS)
|
|
120
181
|
Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
|
|
121
|
-
URLs Puppeteer full browser rendering (auto-detected
|
|
182
|
+
URLs Puppeteer full browser rendering (auto-detected;
|
|
183
|
+
http(s):// and file:// URLs)
|
|
122
184
|
|
|
123
185
|
Examples:
|
|
124
186
|
impeccable detect src/
|
|
@@ -138,6 +200,7 @@ async function detectCli() {
|
|
|
138
200
|
const jsonMode = args.includes('--json');
|
|
139
201
|
const quietMode = args.includes('--quiet');
|
|
140
202
|
const helpMode = args.includes('--help');
|
|
203
|
+
const noAdvisory = args.includes('--no-advisory');
|
|
141
204
|
// --fast (regex-only) is deprecated: since the jsdom removal, the static
|
|
142
205
|
// HTML/CSS analysis is fast and covers every rule, so the regex-only path
|
|
143
206
|
// only loses coverage for no real speed win. Accept the flag for back-compat
|
|
@@ -147,13 +210,15 @@ async function detectCli() {
|
|
|
147
210
|
'Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n',
|
|
148
211
|
);
|
|
149
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
|
+
}
|
|
150
218
|
const configEnabled = !args.includes('--no-config');
|
|
151
219
|
const detectionConfig = configEnabled
|
|
152
220
|
? readDetectionConfig(process.cwd())
|
|
153
221
|
: { ignoreRules: [], ignoreFiles: [], ignoreValues: [] };
|
|
154
|
-
const providers = [];
|
|
155
|
-
if (args.includes('--gpt')) providers.push('gpt');
|
|
156
|
-
if (args.includes('--gemini')) providers.push('gemini');
|
|
157
222
|
const scopes = [];
|
|
158
223
|
for (let i = 0; i < args.length; i++) {
|
|
159
224
|
if (args[i] !== '--scope' && !args[i].startsWith('--scope=')) continue;
|
|
@@ -174,6 +239,20 @@ async function detectCli() {
|
|
|
174
239
|
args.splice(i, inline ? 1 : 2);
|
|
175
240
|
i -= 1;
|
|
176
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
|
+
}
|
|
177
256
|
const unknownScopes = scopes.filter(s => !RULE_SCOPES.has(s));
|
|
178
257
|
if (unknownScopes.length > 0) {
|
|
179
258
|
process.stderr.write(
|
|
@@ -182,13 +261,23 @@ async function detectCli() {
|
|
|
182
261
|
process.exit(1);
|
|
183
262
|
}
|
|
184
263
|
const designSystemEnabled = configEnabled && !args.includes('--no-design-system') && detectionConfig.designSystem?.enabled !== false;
|
|
185
|
-
const designSystem = designSystemEnabled ? loadDesignSystemForCwd(process.cwd()) : null;
|
|
186
264
|
// Inline `impeccable-disable*` waivers are part of the scanned file, so they
|
|
187
265
|
// apply by default. `--no-config` (raw scan) and the dedicated
|
|
188
266
|
// `--no-inline-ignores` both turn them off.
|
|
189
267
|
const inlineIgnoresEnabled = configEnabled && !args.includes('--no-inline-ignores');
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
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
|
+
};
|
|
192
281
|
const targets = args.filter(a => !a.startsWith('--'));
|
|
193
282
|
|
|
194
283
|
if (helpMode) { printUsage(); process.exit(0); }
|
|
@@ -196,19 +285,31 @@ async function detectCli() {
|
|
|
196
285
|
let allFindings = [];
|
|
197
286
|
|
|
198
287
|
if (!process.stdin.isTTY && targets.length === 0) {
|
|
199
|
-
allFindings = await handleStdin(
|
|
288
|
+
allFindings = await handleStdin(scanOptionsFor);
|
|
200
289
|
} else {
|
|
201
290
|
const paths = targets.length > 0 ? targets : [process.cwd()];
|
|
202
|
-
|
|
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;
|
|
203
297
|
const browserDetector = urlTargetCount > 1 ? await createBrowserDetector() : null;
|
|
204
298
|
|
|
205
299
|
try {
|
|
206
300
|
for (const target of paths) {
|
|
207
|
-
if (
|
|
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;
|
|
208
309
|
try {
|
|
209
310
|
const scanner = browserDetector
|
|
210
|
-
? (url) => browserDetector.detectUrl(url,
|
|
211
|
-
: (url) => detectUrl(url,
|
|
311
|
+
? (url) => browserDetector.detectUrl(url, urlOptions)
|
|
312
|
+
: (url) => detectUrl(url, urlOptions);
|
|
212
313
|
allFindings.push(...await scanner(target));
|
|
213
314
|
} catch (e) { process.stderr.write(`Error: ${e.message}\n`); }
|
|
214
315
|
continue;
|
|
@@ -274,11 +375,14 @@ async function detectCli() {
|
|
|
274
375
|
|
|
275
376
|
for (const file of files) {
|
|
276
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);
|
|
277
381
|
let fileFindings;
|
|
278
382
|
if (HTML_EXTENSIONS.has(ext)) {
|
|
279
|
-
fileFindings = await detectHtml(file,
|
|
383
|
+
fileFindings = await detectHtml(file, fileOptions);
|
|
280
384
|
} else {
|
|
281
|
-
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file,
|
|
385
|
+
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, fileOptions);
|
|
282
386
|
}
|
|
283
387
|
// Annotate findings with import context
|
|
284
388
|
const importers = importedByMap.get(file);
|
|
@@ -293,10 +397,11 @@ async function detectCli() {
|
|
|
293
397
|
} else if (stat.isFile()) {
|
|
294
398
|
if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
|
|
295
399
|
const ext = path.extname(resolved).toLowerCase();
|
|
400
|
+
const fileOptions = scanOptionsFor(resolved);
|
|
296
401
|
if (HTML_EXTENSIONS.has(ext)) {
|
|
297
|
-
allFindings.push(...await detectHtml(resolved,
|
|
402
|
+
allFindings.push(...await detectHtml(resolved, fileOptions));
|
|
298
403
|
} else {
|
|
299
|
-
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved,
|
|
404
|
+
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, fileOptions));
|
|
300
405
|
}
|
|
301
406
|
}
|
|
302
407
|
}
|
|
@@ -307,12 +412,24 @@ async function detectCli() {
|
|
|
307
412
|
|
|
308
413
|
allFindings = filterDetectionFindings(allFindings, detectionConfig);
|
|
309
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);
|
|
310
422
|
|
|
311
423
|
if (allFindings.length > 0) {
|
|
312
424
|
if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');
|
|
313
|
-
else if (quietMode)
|
|
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
|
+
}
|
|
314
431
|
else process.stderr.write(formatFindings(allFindings, false) + '\n');
|
|
315
|
-
process.exit(2);
|
|
432
|
+
process.exit(primary.length > 0 ? 2 : 0);
|
|
316
433
|
}
|
|
317
434
|
if (jsonMode) process.stdout.write('[]\n');
|
|
318
435
|
process.exit(0);
|