diff --git a/README.md b/README.md
index b957b7e5b..69cb68639 100644
--- a/README.md
+++ b/README.md
@@ -427,6 +427,8 @@ npx impeccable ignores add-value overused-font Inter --reason "Brand font"
The detector catches 61 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more).
+Human-readable findings are diagnostics written to stderr, so redirect them with `2> findings.txt`. Use `--json` for machine-readable results on stdout. URL scans inspect the rendered DOM, computed layout, and accessible linked stylesheets; browser security still prevents reading cross-origin CSS without CORS. A clean detector run is evidence, not proof of visual or accessibility quality: it does not replace inspecting the rendered experience across relevant viewports.
+
By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution.
For a waiver that should travel with one file instead of the repo config, add an inline comment in the file: ``. The marker works in any comment syntax, scopes to the whole file (or one line with `impeccable-disable-line` / `impeccable-disable-next-line`), and is bypassed by `--no-inline-ignores` or `--no-config`.
diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs
index febf7f297..a9b9fd18a 100644
--- a/cli/engine/browser/injected/index.mjs
+++ b/cli/engine/browser/injected/index.mjs
@@ -1235,7 +1235,7 @@ if (IS_BROWSER) {
// Advisory findings (em-dash overuse, etc.) are surfaced but never
// treated as failures; carry the flag so the overlay/extension can
// render them with the mildest affordance and consumers can filter.
- advisory: (ap && ap.advisory === true) || f.advisory === true,
+ advisory: ap?.severity === 'advisory' || f.severity === 'advisory' || f.advisory === true,
detail: f.detail || f.snippet,
ignoreValue: f.ignoreValue || f.value || '',
name: ap ? ap.name : (f.type || f.id),
@@ -1277,6 +1277,36 @@ if (IS_BROWSER) {
else groupMap.set(el, [...kept]);
}
+ // Read CSS that is absent from document.outerHTML. Inline ${primary}${secondary}`);
+ await fontPage.evaluate(() => { window.__IMPECCABLE_CONFIG__ = { autoScan: false }; });
+ await fontPage.evaluate(browserScript);
+ const fontFindings = await fontPage.evaluate(() => window.impeccableDetect({ serialize: true })
+ .flatMap(group => group.findings || [])
+ .filter(finding => finding.type === 'overused-font'));
+ assert.equal(fontFindings.length, 1, JSON.stringify(fontFindings));
+ assert.match(fontFindings[0].detail, /Primary font: geist \(82% of text\)/i);
+ assert.doesNotMatch(fontFindings[0].detail, /geist mono/i);
+ await fontPage.close();
+ } finally {
+ await browser.close().catch(() => {});
+ }
+ });
+
// Only a real browser reproduces this one: Chrome keeps oklch(), lch(), and
// color(srgb ...) verbatim in getComputedStyle output, so a detector that
// cannot parse those reads every surface as unset, walks out of the page,
diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js
index 9ad3a7a47..30c5e8f18 100644
--- a/tests/detect-antipatterns.test.js
+++ b/tests/detect-antipatterns.test.js
@@ -2717,23 +2717,30 @@ describe('CLI', () => {
expect(code).toBe(0);
expect(stdout).toContain('Usage:');
expect(stdout).toContain('--quiet');
+ expect(stdout).toContain('Human-readable findings go to stderr');
expect(stdout).not.toContain('--gpt');
expect(stdout).not.toContain('--gemini');
});
- test('generated-UI tells run by default in the CLI', () => {
+ test('severity advisory is non-blocking, flagged in JSON, and suppressible', () => {
const { stdout, code } = run('--json', path.join(FIXTURES, 'gpt-tells.html'));
- expect(code).toBe(2);
- const ids = JSON.parse(stdout).map(f => f.antipattern);
+ expect(code).toBe(0);
+ const findings = JSON.parse(stdout);
+ const ids = findings.map(f => f.antipattern);
expect(ids).toContain('gpt-thin-border-wide-shadow');
expect(ids).toContain('repeating-stripes-gradient');
expect(ids).toContain('codex-grid-background');
expect(ids).toContain('theater-slop-phrase');
+ expect(findings.every(f => f.severity === 'advisory' && f.advisory === true)).toBe(true);
+
+ const hidden = run('--json', '--no-advisory', path.join(FIXTURES, 'gpt-tells.html'));
+ expect(hidden.code).toBe(0);
+ expect(JSON.parse(hidden.stdout)).toEqual([]);
});
test('legacy provider flags are accepted as deprecated no-ops', () => {
const { stdout, stderr, code } = run('--gpt', '--json', path.join(FIXTURES, 'gpt-tells.html'));
- expect(code).toBe(2);
+ expect(code).toBe(0);
expect(stderr).toContain('--gpt and --gemini are deprecated and ignored');
expect(JSON.parse(stdout).some(f => f.antipattern === 'codex-grid-background')).toBe(true);
});
@@ -2744,14 +2751,30 @@ describe('CLI', () => {
expect(stderr).not.toContain('cannot access detect');
});
+ test('keeps a local path containing spaces as one scan target', () => {
+ const fixture = writeStaticFixture({
+ 'page with spaces.html': '