Port: fail URL scans when the browser is unavailable (#711)

Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans
when the browser is unavailable).

`detect` gains an operational-failure flag. Exit 1 now means at least one
requested target could not be scanned, and it takes precedence over exit 2,
because findings from the targets that did scan do not turn a partial scan
into a complete one. The flag is set by an unreachable path, an unreadable
directory or file in a dir walk, a per-file scan that throws, a URL scan
that throws, and a shared-browser setup failure.

- `walk_dir_reporting` and `build_import_graph_reporting` take a read-error
  callback; the plain wrappers stay for callers that do not report. A file
  the graph could not read is skipped for the scan too.
- `SharedBrowser::ensure_launched` is the eager half of
  `createBrowserDetector()`: the CLI brings the browser up before the loop so
  a launch failure prints one `Error:` line and every URL target is skipped,
  instead of the lazy launch reporting once per URL.
- The static engine and the text path spell a permission failure the way Node
  does (`EACCES: permission denied, open '<path>'`), which is what
  `Error: cannot scan <target>: <message>` prints.
- Usage text and docs/CLI-CONTRACT.md carry the exit-status block.

Verified against origin/main's JS: missing target, missing target alongside a
flagging file, unreadable file, unreadable file beside a readable sibling,
unreadable directory, unreadable nested directory, a clean scan, and a
browser-unavailable scan of one and of two URLs all agree on exit code,
stdout and stderr (the browser-not-found wording is the pre-existing
puppeteer-vs-discovery difference).

Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at
exit 1, plus new `detect-missing-file-with-findings`,
`detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each
cross-checked against origin/main. `detect-help` carries the new block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
Paul Bakaus
2026-09-03 12:56:48 -07:00
co-authored by Claude Fable 5.1
parent bc45026706
commit ed50cc5ea0
13 changed files with 264 additions and 37 deletions
+19
View File
@@ -133,6 +133,25 @@ impl SharedBrowser for SharedBrowserHandle<'_> {
b.close();
}
}
fn ensure_launched(&self) -> Result<(), EngineError> {
if let Some(msg) = self.launch_error.borrow().as_ref() {
return Err(EngineError::new(msg.clone()));
}
if self.browser.borrow().is_some() {
return Ok(());
}
match self.engine.launch() {
Ok(b) => {
*self.browser.borrow_mut() = Some(b);
Ok(())
}
Err(e) => {
*self.launch_error.borrow_mut() = Some(e.message.clone());
Err(e)
}
}
}
}
/// JS detect-url.mjs `credentials` from `splitScanUrl`.
+133 -24
View File
@@ -15,10 +15,11 @@ use crate::design_system::{load_design_system_for_target, DesignSystemCache};
use crate::detect_text::{detect_text, TextOptions};
use crate::engines::{EngineError, Engines, ScanOptions};
use crate::file_system::{
build_import_graph, detect_framework_config, is_html_path, is_port_listening, walk_dir,
build_import_graph_reporting, detect_framework_config, is_html_path, is_port_listening,
walk_dir_reporting,
};
use crate::jsp;
use crate::util::{exists, re, read_text, D};
use crate::util::{exists, re, D};
pub const USAGE: &str = "Usage: impeccable detect [options] [file-or-dir-or-url...]
@@ -47,6 +48,12 @@ Output streams:
Human-readable findings go to stderr so stdout stays available for structured
output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.
Exit status:
0 Scan completed with no primary findings (advisories may still be listed)
1 At least one requested target could not be scanned
2 Scan completed with primary findings
Operational failure takes precedence when a multi-target scan is partial.
Project config:
Respects .impeccable/config.json and .impeccable/config.local.json detector
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
@@ -231,9 +238,18 @@ struct Ctx<'a> {
base: ScanOptions,
cache: DesignSystemCache,
stdin_tty: bool,
/// JS `hadOperationalFailure`: at least one requested target could not be
/// scanned, which forces exit 1 (#711).
had_operational_failure: bool,
}
impl<'a> Ctx<'a> {
/// JS: main.mjs#reportLocalScanFailure
fn report_local_scan_failure(&mut self, target: &str, message: &str) {
self.had_operational_failure = true;
self.io.err(&format!("Error: cannot scan {target}: {message}\n"));
}
fn scan_options_for(&mut self, local_path: Option<&str>) -> ScanOptions {
let (Some(local_path), true) = (local_path, self.design_system_enabled) else {
return self.base.clone();
@@ -263,11 +279,18 @@ impl<'a> Ctx<'a> {
.html
.detect_html(file_path, options, &mut *self.io.stderr);
}
let content = read_text(file_path).ok_or_else(|| {
EngineError::new(format!(
"ENOENT: no such file or directory, open '{file_path}'"
))
})?;
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
// JS `fs.readFileSync` throws with Node's errno message.
return Err(EngineError::new(match e.kind() {
std::io::ErrorKind::PermissionDenied => {
format!("EACCES: permission denied, open '{file_path}'")
}
_ => format!("ENOENT: no such file or directory, open '{file_path}'"),
}));
}
};
Ok(detect_text(
&content,
file_path,
@@ -518,6 +541,7 @@ fn detect_cli(args_in: &[String], io: &mut Io, engines: &Engines) -> Result<i32,
base,
cache: DesignSystemCache::new(),
stdin_tty,
had_operational_failure: false,
};
let mut all: Vec<Finding> = Vec::new();
@@ -530,12 +554,33 @@ fn detect_cli(args_in: &[String], io: &mut Io, engines: &Engines) -> Result<i32,
targets.clone()
};
let url_count = paths.iter().filter(|p| URL_RE.is_match(p)).count();
let shared = if url_count > 1 {
let mut shared = if url_count > 1 {
engines.url.and_then(|u| u.open_shared())
} else {
None
};
let result = scan_targets(&mut ctx, &paths, shared.as_deref(), &mut all);
// JS: `await createBrowserDetector()` throws before the loop; the
// failure is reported once and every URL target is skipped (#711).
let mut browser_setup_failed = false;
if let Some(s) = shared.as_deref() {
if let Err(e) = s.ensure_launched() {
browser_setup_failed = true;
ctx.had_operational_failure = true;
ctx.io.err(&format!("Error: {}\n", e.message));
}
}
if browser_setup_failed {
if let Some(s) = shared.take() {
s.close();
}
}
let result = scan_targets(
&mut ctx,
&paths,
shared.as_deref(),
browser_setup_failed,
&mut all,
);
if let Some(s) = shared {
s.close();
}
@@ -550,6 +595,16 @@ fn detect_cli(args_in: &[String], io: &mut Io, engines: &Engines) -> Result<i32,
}
let (primary, advisory) = partition_advisory(&all);
let (primary_len, advisory_len) = (primary.len(), advisory.len());
// Exit 1 means at least one requested scan could not complete. It takes
// precedence over exit 2 because findings from the remaining targets do
// not turn a partial scan into a complete one (#711).
let exit_code = if ctx.had_operational_failure {
1
} else if primary_len > 0 {
2
} else {
0
};
if !all.is_empty() {
if json_mode {
let text = format_findings(&all, true, stderr_tty);
@@ -571,12 +626,31 @@ fn detect_cli(args_in: &[String], io: &mut Io, engines: &Engines) -> Result<i32,
let text = format_findings(&all, false, stderr_tty);
ctx.io.err(&format!("{text}\n"));
}
return Ok(if primary_len > 0 { 2 } else { 0 });
return Ok(exit_code);
}
if json_mode {
ctx.io.out("[]\n");
}
Ok(0)
Ok(exit_code)
}
/// The `error.message` Node hands `reportLocalScanFailure` for a failed
/// `readdirSync` / `readFileSync`.
fn node_scan_error(path: &str, err: &std::io::Error) -> String {
let syscall = if std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false) {
"scandir"
} else {
"open"
};
match err.kind() {
std::io::ErrorKind::NotFound => {
format!("ENOENT: no such file or directory, {syscall} '{path}'")
}
std::io::ErrorKind::PermissionDenied => {
format!("EACCES: permission denied, {syscall} '{path}'")
}
_ => format!("{err}"),
}
}
/// An engine error outside a per-URL try: the JS lets it propagate to
@@ -590,10 +664,14 @@ fn scan_targets(
ctx: &mut Ctx,
paths: &[String],
shared: Option<&dyn crate::engines::SharedBrowser>,
browser_setup_failed: bool,
all: &mut Vec<Finding>,
) -> Result<(), Exit> {
for target in paths {
if URL_RE.is_match(target) {
if browser_setup_failed {
continue;
}
let url_options = if FILE_URL_RE.is_match(target) {
let local = file_url_to_local_path(target);
ctx.scan_options_for(local.as_deref())
@@ -611,12 +689,16 @@ fn scan_targets(
};
match result {
Ok(f) => all.extend(f),
Err(e) => ctx.io.err(&format!("Error: {}\n", e.message)),
Err(e) => {
ctx.had_operational_failure = true;
ctx.io.err(&format!("Error: {}\n", e.message));
}
}
continue;
}
let resolved = jsp::resolve(&ctx.cwd, &[target]);
let Ok(stat) = std::fs::metadata(&resolved) else {
ctx.had_operational_failure = true;
ctx.io.err(&format!("Warning: cannot access {target}\n"));
continue;
};
@@ -649,10 +731,18 @@ fn scan_targets(
}
}
let cwd = ctx.cwd.clone();
let files: Vec<String> = walk_dir(&resolved)
.into_iter()
.filter(|f| !should_ignore_detection_file(f, &cwd, &ctx.config))
.collect();
// Unreadable directories and files are reported, not silently
// skipped, and each one forces exit 1 (#711).
let mut walk_failures: Vec<(String, String)> = Vec::new();
let files: Vec<String> = walk_dir_reporting(&resolved, &mut |dir, err| {
walk_failures.push((dir.to_string(), node_scan_error(dir, err)));
})
.into_iter()
.filter(|f| !should_ignore_detection_file(f, &cwd, &ctx.config))
.collect();
for (dir, message) in walk_failures {
ctx.report_local_scan_failure(&dir, &message);
}
let html_count = files.iter().filter(|f| is_html_path(f)).count();
if files.len() > 50 && ctx.stdin_tty && !ctx.json_mode && !ctx.quiet_mode {
ctx.io.err(&format!(
@@ -667,7 +757,15 @@ fn scan_targets(
return Err(Exit(0));
}
}
let graph = build_import_graph(&files);
let mut unreadable_files: Vec<String> = Vec::new();
let mut read_failures: Vec<(String, String)> = Vec::new();
let graph = build_import_graph_reporting(&files, &mut |file, err| {
unreadable_files.push(file.to_string());
read_failures.push((file.to_string(), node_scan_error(file, err)));
});
for (file, message) in read_failures {
ctx.report_local_scan_failure(&file, &message);
}
let mut imported_by_map: Vec<(String, Vec<String>)> = Vec::new();
for (importer, imports) in &graph {
for imported in imports {
@@ -681,10 +779,18 @@ fn scan_targets(
}
}
for file in &files {
if unreadable_files.contains(file) {
continue;
}
let opts = ctx.scan_options_for(Some(file));
let mut file_findings = ctx
.detect_local_file(file, &opts)
.map_err(|e| fatal(ctx.io, e))?;
let mut file_findings = match ctx.detect_local_file(file, &opts) {
Ok(f) => f,
Err(e) => {
let message = e.message.clone();
ctx.report_local_scan_failure(file, &message);
continue;
}
};
if let Some((_, importers)) = imported_by_map.iter().find(|(k, _)| k == file) {
if !importers.is_empty() {
let names: Vec<Value> = importers
@@ -705,10 +811,13 @@ fn scan_targets(
continue;
}
let opts = ctx.scan_options_for(Some(&resolved));
let f = ctx
.detect_local_file(&resolved, &opts)
.map_err(|e| fatal(ctx.io, e))?;
all.extend(f);
match ctx.detect_local_file(&resolved, &opts) {
Ok(f) => all.extend(f),
Err(e) => {
let message = e.message.clone();
ctx.report_local_scan_failure(target, &message);
}
}
}
}
Ok(())
+7
View File
@@ -76,6 +76,13 @@ pub trait UrlEngine {
pub trait SharedBrowser {
fn detect_url(&self, url: &str, options: &ScanOptions) -> Result<Vec<Finding>, EngineError>;
fn close(&self);
/// The eager half of `createBrowserDetector()`: bring the browser up now,
/// so a launch failure is reported once before the loop and every URL
/// target is skipped, exactly as the JS `await createBrowserDetector()`
/// throw does (#711). Engines with nothing to launch keep the default.
fn ensure_launched(&self) -> Result<(), EngineError> {
Ok(())
}
}
/// The engines available to one `detect` run.
+33 -7
View File
@@ -57,9 +57,22 @@ pub fn is_html_path(file_path: &str) -> bool {
/// JS: file-system.mjs#walkDir. Files in `readdirSync` order (the OS order,
/// which Node does not sort either), recursive; an unreadable dir yields [].
pub fn walk_dir(dir: &str) -> Vec<String> {
walk_dir_reporting(dir, &mut |_, _| {})
}
/// JS: file-system.mjs#walkDir(dir, onReadError). An unreadable directory is
/// reported and skipped rather than silently yielding nothing (#711).
pub fn walk_dir_reporting(
dir: &str,
on_read_error: &mut dyn FnMut(&str, &std::io::Error),
) -> Vec<String> {
let mut files = Vec::new();
let Ok(rd) = std::fs::read_dir(dir) else {
return files;
let rd = match std::fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) => {
on_read_error(dir, &e);
return files;
}
};
let mut entries: Vec<(String, bool)> = Vec::new();
for entry in rd.flatten() {
@@ -82,7 +95,7 @@ pub fn walk_dir(dir: &str) -> Vec<String> {
}
let full = jsp::join(&[dir, &name]);
if is_dir {
files.extend(walk_dir(&full));
files.extend(walk_dir_reporting(&full, on_read_error));
} else if has_scannable_extension(&name) {
files.push(full);
}
@@ -134,12 +147,25 @@ pub fn resolve_import(specifier: &str, from_dir: &str, file_set: &[String]) -> O
/// JS: file-system.mjs#buildImportGraph. `(file, imports)` pairs in file
/// order; each import list is insertion-ordered and deduplicated (JS `Set`).
pub fn build_import_graph(files: &[String]) -> Vec<(String, Vec<String>)> {
build_import_graph_reporting(files, &mut |_, _| {})
}
/// JS: file-system.mjs#buildImportGraph(files, onReadError). A file that
/// cannot be read is reported and left out of the graph; the caller skips it
/// for the scan too (#711).
pub fn build_import_graph_reporting(
files: &[String],
on_read_error: &mut dyn FnMut(&str, &std::io::Error),
) -> Vec<(String, Vec<String>)> {
let mut graph = Vec::new();
for file in files {
// JS readFileSync throws on an unreadable file and the whole scan
// aborts; a file that vanished between walk and read is rare enough
// that treating it as empty is the friendlier port.
let content = read_text(file).unwrap_or_default();
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
on_read_error(file, &e);
continue;
}
};
let dir = jsp::dirname(file);
let mut imports: Vec<String> = Vec::new();
for pattern in IMPORT_SPECIFIER_PATTERNS.iter() {
+3
View File
@@ -88,6 +88,9 @@ impl HtmlEngine for StaticHtmlEngine {
std::io::ErrorKind::NotFound => {
format!("ENOENT: no such file or directory, open '{path}'")
}
std::io::ErrorKind::PermissionDenied => {
format!("EACCES: permission denied, open '{path}'")
}
_ => format!("{source}, open '{path}'"),
},
})
+19 -3
View File
@@ -134,6 +134,12 @@ Output streams:
Human-readable findings go to stderr so stdout stays available for structured
output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.
Exit status:
0 Scan completed with no primary findings (advisories may still be listed)
1 At least one requested target could not be scanned
2 Scan completed with primary findings
Operational failure takes precedence when a multi-target scan is partial.
Project config:
Respects .impeccable/config.json and .impeccable/config.local.json detector
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
@@ -179,15 +185,25 @@ Examples:
- **File**: skipped if `shouldIgnoreDetectionFile`; else `detectLocalFile`.
- `detectLocalFile(fp, opts)`: extension (lowercased) in `HTML_EXTENSIONS = {'.html','.htm'}``detectHtml(fp, opts)`; else `detectText(readFileSync(fp,'utf-8'), fp, opts)`.
4. Post-filter: `filterDetectionFindings(all, config)` (ignoreRules/ignoreValues), then `filterByScopes(all, scopes)` (keeps findings whose rule declares any requested scope; empty scopes = no filter), then `--no-advisory` drop.
5. Partition `{primary, advisory}` by `f.advisory === true`.
5. Partition `{primary, advisory}` by `f.advisory === true || f.severity === 'advisory'`.
Any target that cannot be scanned sets `hadOperationalFailure` (#711): a URL
whose browser setup or scan throws, a path `statSync` cannot reach
(`stderr> Warning: cannot access <target>`), an unreadable directory or file
in a dir walk, and a per-file scan that throws
(`stderr> Error: cannot scan <target>: <message>`). A multi-URL scan whose
shared browser fails to launch prints its `Error:` once and skips every URL
target.
**Output and exit codes**:
- `allFindings.length > 0`:
- json: `stdout> JSON.stringify(allFindings, null, 2) + '\n'` (all findings, advisory ones flagged).
- quiet: `stderr> ${primary.length} anti-pattern${n===1?'':'s'} found.\n`; if advisory: `stderr> dim(`${adv} advisory note${adv===1?'':'s'} (not counted).`) + '\n'`.
- text: `stderr> formatFindings(all,false) + '\n'`.
- `exit(primary.length > 0 ? 2 : 0)`.
- no findings: json → `stdout> []\n`; text/quiet → nothing. `exit 0`.
- `exit(hadOperationalFailure ? 1 : (primary.length > 0 ? 2 : 0))`.
- no findings: json → `stdout> []\n`; text/quiet → nothing. `exit(hadOperationalFailure ? 1 : 0)`.
- Exit 1 takes precedence over exit 2: findings from the targets that did scan
do not turn a partial scan into a complete one (#711).
- Any other exit: `1` for arg errors above; uncaught exceptions propagate to `cli.js` catch (`exit 1`).
- `dim(text)` = `process.stderr.isTTY ? '\x1b[2m' + text + '\x1b[0m' : text`. This is the **only** ANSI styling in detect output.
+26
View File
@@ -56,6 +56,32 @@ export default function cases() {
{ id: 'detect-no-args', verb: 'detect', args: [] },
{ id: 'detect-missing-file', verb: 'detect', args: ['--no-config', 'does-not-exist.html'] },
{ id: 'detect-missing-file-json', verb: 'detect', args: ['--no-config', '--json', 'does-not-exist.html'] },
// #711: a target that cannot be scanned forces exit 1, and that takes
// precedence over findings from the targets that did scan.
{
id: 'detect-missing-file-with-findings', verb: 'detect',
args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/layout.html`, 'does-not-exist.html'],
isolateHome: false,
},
{
id: 'detect-unreadable-file-json', verb: 'detect',
setup: (ws) => {
const p = path.join(ws, 'locked.html');
fs.writeFileSync(p, '<div style="border-left: 4px solid #ff0000">x</div>\n');
fs.chmodSync(p, 0o000);
},
args: ['--no-config', '--json', 'locked.html'],
},
{
id: 'detect-unreadable-file-in-dir', verb: 'detect',
setup: (ws) => {
fs.writeFileSync(path.join(ws, 'a.html'), '<div style="border-left: 4px solid #ff0000">x</div>\n');
const p = path.join(ws, 'b.html');
fs.writeFileSync(p, '<div style="border-left: 4px solid #ff0000">x</div>\n');
fs.chmodSync(p, 0o000);
},
args: ['--no-config', '--json', '.'],
},
{ id: 'detect-unknown-flag', verb: 'detect', args: ['--bogus', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
{ id: 'detect-bad-viewport', verb: 'detect', args: ['--viewport', 'wide', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
{ id: 'cli-help', verb: 'cli-help', args: [] },
+1 -1
View File
@@ -1,5 +1,5 @@
{
"stdout": "Usage: impeccable detect [options] [file-or-dir-or-url...]\n\nScan files or URLs for UI anti-patterns and design quality issues.\n\nOptions:\n --json Output results as JSON\n --quiet In text mode, only print the final findings count\n --scope <name> Only report rules in the given design domain\n (type, layout). Comma-separated.\n --viewport <WxH> Browser viewport for URL scans (default 1280x800),\n e.g. --viewport 390x844 for a mobile-width pass\n --no-config Do not apply project config, detector ignores, inline\n ignore comments, or DESIGN.md\n --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments\n --no-design-system Do not load local DESIGN.md / .impeccable/design.json context\n --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)\n --help Show this help message\n\nAdvisory findings:\n Some rules are advisory: detected and listed in a separate section, but never\n counted as failures and never changing the exit code. They stay out of the\n failure count so they never block automation. --no-advisory hides them.\n\nOutput streams:\n Human-readable findings go to stderr so stdout stays available for structured\n output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.\n\nProject config:\n Respects .impeccable/config.json and .impeccable/config.local.json detector\n settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,\n and detector.designSystem.enabled.\n\nInline ignores:\n In-file comments waive a finding where it lives and travel with the file:\n <!-- impeccable-disable overused-font -- exported brand doc -->\n .brand { font-family: Inter } /* impeccable-disable-line overused-font */\n // impeccable-disable-next-line bounce-easing: intentional bounce\n impeccable-disable applies to the whole file; -line / -next-line are scoped.\n List one or more rule ids (comma-separated), or omit them / use * for all.\n\nDetection modes:\n HTML files Static HTML/CSS analysis (default, catches linked CSS)\n Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)\n URLs Puppeteer full browser rendering (auto-detected;\n http(s):// and file:// URLs; accessible linked CSS included)\n\nExamples:\n impeccable detect src/\n impeccable detect index.html\n impeccable detect https://example.com\n impeccable detect --json .\n impeccable detect --no-config src/\n",
"stdout": "Usage: impeccable detect [options] [file-or-dir-or-url...]\n\nScan files or URLs for UI anti-patterns and design quality issues.\n\nOptions:\n --json Output results as JSON\n --quiet In text mode, only print the final findings count\n --scope <name> Only report rules in the given design domain\n (type, layout). Comma-separated.\n --viewport <WxH> Browser viewport for URL scans (default 1280x800),\n e.g. --viewport 390x844 for a mobile-width pass\n --no-config Do not apply project config, detector ignores, inline\n ignore comments, or DESIGN.md\n --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments\n --no-design-system Do not load local DESIGN.md / .impeccable/design.json context\n --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)\n --help Show this help message\n\nAdvisory findings:\n Some rules are advisory: detected and listed in a separate section, but never\n counted as failures and never changing the exit code. They stay out of the\n failure count so they never block automation. --no-advisory hides them.\n\nOutput streams:\n Human-readable findings go to stderr so stdout stays available for structured\n output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.\n\nExit status:\n 0 Scan completed with no primary findings (advisories may still be listed)\n 1 At least one requested target could not be scanned\n 2 Scan completed with primary findings\n Operational failure takes precedence when a multi-target scan is partial.\n\nProject config:\n Respects .impeccable/config.json and .impeccable/config.local.json detector\n settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,\n and detector.designSystem.enabled.\n\nInline ignores:\n In-file comments waive a finding where it lives and travel with the file:\n <!-- impeccable-disable overused-font -- exported brand doc -->\n .brand { font-family: Inter } /* impeccable-disable-line overused-font */\n // impeccable-disable-next-line bounce-easing: intentional bounce\n impeccable-disable applies to the whole file; -line / -next-line are scoped.\n List one or more rule ids (comma-separated), or omit them / use * for all.\n\nDetection modes:\n HTML files Static HTML/CSS analysis (default, catches linked CSS)\n Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)\n URLs Puppeteer full browser rendering (auto-detected;\n http(s):// and file:// URLs; accessible linked CSS included)\n\nExamples:\n impeccable detect src/\n impeccable detect index.html\n impeccable detect https://example.com\n impeccable detect --json .\n impeccable detect --no-config src/\n",
"stderr": "",
"exit": 0,
"signal": null,
@@ -1,7 +1,7 @@
{
"stdout": "[]\n",
"stderr": "Warning: cannot access does-not-exist.html\n",
"exit": 0,
"exit": 1,
"signal": null,
"files": {}
}
@@ -0,0 +1,7 @@
{
"stdout": "[\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n }\n]\n",
"stderr": "Warning: cannot access does-not-exist.html\n",
"exit": 1,
"signal": null,
"files": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"stdout": "",
"stderr": "Warning: cannot access does-not-exist.html\n",
"exit": 0,
"exit": 1,
"signal": null,
"files": {}
}
@@ -0,0 +1,7 @@
{
"stdout": "[\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/a.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 4px\"\n }\n]\n",
"stderr": "Error: cannot scan <WS>/b.html: EACCES: permission denied, open '<WS>/b.html'\n",
"exit": 1,
"signal": null,
"files": {}
}
@@ -0,0 +1,7 @@
{
"stdout": "[]\n",
"stderr": "Error: cannot scan locked.html: EACCES: permission denied, open '<WS>/locked.html'\n",
"exit": 1,
"signal": null,
"files": {}
}