# Impeccable CLI contract The observable behavior of every `impeccable` verb: arguments, environment, inputs, stdout and stderr byte for byte, exit codes, files written, network. This is the specification an alternate implementation of the scripts has to meet; `tests/oracle/` records goldens against it and replays them. Verb names are the binary's subcommands. Each verb was a script under `skill/scripts/` or a `cli/bin` subcommand when this contract was recorded; the mapping is `JS_VERBS` in `tests/oracle/lib.mjs`, and file references below name those scripts as the source the contract was read from. Since the launcher swap the skill invokes every verb as `{{scripts_path}}/impeccable ` and the scripts are gone from the tree; the contract stands as written. Quoted strings, regexes, and JSON in this document are verbatim from the source, including any em dashes inside user-facing messages; do not "fix" them. Sections: 1. Detect CLI and misc verbs (`detect`, `ignores`, `skills`, `concept-seed`, `serve-question`, `generate-image`) 2. Context and utility verbs (`context`, `doctor`, `pin`, `surface-brief`, `critique-storage`, `palette`, `embed-prompt`, `signals`, `detect-csp`) 3. Design hook (`hook`, `hook-before-edit`, `hooks`) 4. Live mode (`live*`) --- ## 1. Detect CLI and misc verbs Source snapshot: repo `impeccable-second` @ main (f88b2837), 2026-08-17. Node `>=22.18.0`, package `impeccable` v3.6.0, `"type": "module"`, `bin: { impeccable: cli/bin/cli.js }`, `main`/`exports["."]`: `cli/engine/detect-antipatterns.mjs`, `exports["./browser"]`: `cli/engine/detect-antipatterns-browser.js`. Notation: `stdout>` / `stderr>` are literal writes; `exit N`; template strings use `${}` as in the source. All regexes are quoted verbatim. --- ### PART 1 — the `impeccable` binary and the detect engine #### `cli/bin/cli.js` -> `impeccable ` (root dispatcher) - **Invoked from**: README.md "CLI" section (`npx impeccable detect src/`, `npx impeccable ignores ...`), README.npm.md Quick Start, `skill/reference/hooks.md:15` ("Manual `npx impeccable detect` scans use the same project filter config..."), `hooks.md:65` ("Run `npx impeccable detect ` first to see what actually fires there"). - **Args**: `args = process.argv.slice(2)`, `command = args[0]`. - **Dispatch order (exact)**: 1. `!command || command === '--help' || command === '-h'` → print (via `console.log`, stdout) then `exit 0`: ``` Usage: impeccable [options] Commands: detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues ignores Manage detector ignore rules, files, and values help List all available skills and commands install Install impeccable skills into your project or global harness link Symlink skills from a local checkout or submodule update Update skills to the latest version check Check if skill updates are available Options: --help Show this help message --version Show version number Compatibility: impeccable skills Legacy namespace; still supported. ``` 2. `--version` / `-v` → `console.log(pkg.version)` read from `/../../package.json`; `exit 0`. The binary run directly (the skill launcher's path) prints the installed skill's `SKILL.md` frontmatter version instead (skill dir from `IMPECCABLE_SKILL_DIR`, else the binary's path), else `+engine`. 3. `detect` → `process.argv = [argv0, argv1, ...args.slice(1)]`; dynamic-import `../engine/detect-antipatterns.mjs`, `await detectCli()`. 4. `ignores` or `ignore` → `./commands/ignores.mjs` `run(args.slice(1))`. 5. `skills` → `./commands/skills.mjs` `run(args.slice(1))` (legacy namespace). 6. `command ∈ SKILL_COMMANDS = {'help','install','link','update','check'}` → `skills.mjs run(args)` (note: whole `args`, so `run` sees the verb as `args[0]`). 7. `looksLikeDetectTarget(command)` → detect shorthand: `process.argv = [argv0, argv1, ...args]` then `detectCli()`. Predicate: `arg.startsWith('-') || /^https?:\/\//i.test(arg) || arg.includes('/') || arg.includes('\\') || arg.includes('.') || existsSync(resolve(arg))`. 8. `init` → `console.error('"init" is not a CLI command. Type /impeccable init in your AI coding agent\'s chat (Claude Code, Cursor, Codex, ...), not in this terminal.')`, `exit 1`. (Note: a real path literally named `init` hits rule 7 first — tested: "a real path named init still routes to detect".) 9. otherwise → `console.error(\`Unknown command: "${command}"\n\nTo see a list of supported commands, run:\n impeccable --help\`)`, `exit 1`. - **Top-level catch**: `main().catch(error => { if (error?.code === 'IMPECCABLE_PROMPT_ABORT') { console.log('\nAborted.'); exit 130 } console.error(error?.message || error); exit 1 })`. - **No `live` subcommand exists in the CLI** (README/CLAUDE.md mentions of `npx impeccable live` are aspirational; live mode is driven by `skill/scripts/live*.mjs`, out of scope here). - **Tests**: `tests/skills-cli.test.js` ("root help advertises top-level skills commands", "top-level install aliases the legacy skills install command", "#472" init tests, "a real path named init still routes to detect"). --- #### `cli/engine/detect-antipatterns.mjs` (facade) and `skill/scripts/detect.mjs` (wrapper) -> `impeccable detect` - **Facade**: re-exports registry (`ANTIPATTERNS`, `RULE_ENGINE_SUPPORT`, `getAntipattern`, `getRulesForCategory`, `getRuleEngineSupport`), constants (`SAFE_TAGS`, `BORDER_SAFE_TAGS`, `OVERUSED_FONTS`, `GENERIC_FONTS`, `KNOWN_SERIF_FONTS`), color helpers, `isFullPage`, check fns, `createDetectorProfile`, `summarizeDetectorProfile`, design-system fns (`parseFrontmatter as parseDesignFrontmatter`, `normalizeDesignSystem`, `loadDesignSystemForCwd`, `checkSourceDesignSystem`, `collectStaticDesignSystemFindings`), `detectHtml`, `detectUrl`, `createBrowserDetector`, `detectText`, `extractStyleBlocks`, `extractCSSinJS`, fs helpers (`walkDir`, `hasScannableExtension`, `SCANNABLE_EXTENSIONS`, `SKIP_DIRS`, `buildImportGraph`, `resolveImport`, `detectFrameworkConfig`, `isPortListening`, `FRAMEWORK_CONFIGS`), `formatFindings`, `detectCli`. Main-module guard: `process.argv[1]?.endsWith('detect-antipatterns.mjs') || endsWith('detect-antipatterns.mjs/')` → `detectCli()`. - **`skill/scripts/detect.mjs`** (what the skill invokes as `node {{scripts_path}}/detect.mjs ...`): candidates in order `/detector/detect-antipatterns.mjs` (bundled install layout), then `/../../cli/engine/detect-antipatterns.mjs` (repo layout). If neither exists: `stderr> Error: bundled detector not found.\n`, `exit 1`. Else dynamic-imports via `pathToFileURL` and `await detectCli()`. `detectCli` strips a leading `detect` arg itself, so both `detect.mjs --json x` and `detect.mjs detect --json x` work. - **Skill call sites (quoted)**: - `reference/routing.md:16`: "**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `node {{scripts_path}}/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects)." - `reference/critique.md:73`: `node {{scripts_path}}/detect.mjs --json [target]` with "Pass markup files/directories as `[target]`; do not pass CSS-only files. For URLs, skip CLI scan and use browser visualization. ... Exit code 0 = clean; 2 = findings. If the detector entrypoint is missing or fails to load, report deterministic scan unavailable and continue". - `reference/layout.md:28`: `node {{scripts_path}}/detect.mjs --json --scope layout [target files or dirs]`; `reference/typeset.md:27`: `... --scope type ...`. - `reference/audit.native.md:3`: "no browser tooling or `detect.mjs` applies" for native. #### `detectCli()` — `cli/engine/cli/main.mjs` **Arg normalization**: `args = process.argv.slice(2)` with `-json`→`--json`, `-fast`→`--fast`; if `args[0] === 'detect'` drop it. **Flags** (all detected with `args.includes` unless stated): | Flag | Effect | |---|---| | `--json` | JSON output to **stdout** | | `--quiet` | text mode prints only summary line(s) to stderr | | `--github` | GitHub Actions workflow commands to **stdout**, one per finding (`::warning` / `::notice` / `::error file=…,line=…,title=…::[id] snippet%0Adescription`). Paths are relative to `GITHUB_WORKSPACE` when set, else cwd. `line` is omitted when the finding has no line (`0` / NaN). Like `--json` it skips the text-mode notes (purple, scan and regex-only notes), the directory framework note and its port probe, and the >50-files prompt; an attached (`--cdp`) scan's capture notes go to stderr, as under `--json`. Combined with `--json` → `stderr> Error: --github cannot be used with --json\n`, `exit 1`. Combined with `--list-rules` (with or without `--json`) → `stderr> Error: --github annotates scan findings and cannot be used with --list-rules\n`, `exit 1`. Evaluated after `--help` (so `--github --help` prints usage). Exit codes are unchanged | | `--quiet` | text mode prints only summary line(s) to stderr; under `--list-rules` it instead prints one rule id per line to **stdout** | | `--list-rules` | print every registered rule (`all_antipatterns()`: the built-ins plus any installed rule pack) and **exit 0** without scanning. Catalog goes to **stdout**. `--json` pretty-prints an array of `{id,name,category,severity,scopes,description}` (plus `advisory: true` on advisory rules). `--quiet` prints one rule id per line. `--scope` and `--no-advisory` filter the catalog the same way they filter a scan; config ignores (`ignoreRules`) are not applied, so the catalog lists rules a project has switched off. `--github` with it is rejected (see `--github`). A positional target or `--cdp` with it → `stderr> Error: --list-rules prints the rule catalog and takes no file, URL, or --cdp scan\n`, `exit 1`. Evaluated after `--help` (so `--list-rules --help` prints usage) | | `--help` | print usage (stdout) and `exit 0` — evaluated *after* scope/viewport parsing (so a bad `--scope` still errors first) | | `--no-advisory` | drop findings with `advisory === true` before output/exit-code | | `--no-consent-hiding` | URL scans only (Rust engine): leave known consent managers' banners, and a site's own cookie notices, on the page instead of hiding them before the scan (see **Consent banners** under URL scans). No effect on file scans | | `--no-overlay-hiding` | URL scans only (Rust engine): leave product tours, site-builder badges and preloaders on the page, and skip the preloader wait (see **Tours and preloaders** under URL scans). No effect on file scans | | `--fast` | deprecated, ignored; `stderr> Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n` | | `--gpt`, `--gemini` | deprecated, ignored; `stderr> Note: --gpt and --gemini are deprecated and ignored. Generated-UI tells now run by default.\n` | | `--no-config` | `configEnabled=false`: detectionConfig = `{ignoreRules:[],ignoreFiles:[],ignoreValues:[]}`; also disables design system and inline ignores | | `--no-inline-ignores` | `inlineIgnoresEnabled = configEnabled && !flag` | | `--no-design-system` | `designSystemEnabled = configEnabled && !flag && detectionConfig.designSystem?.enabled !== false` | | `--scope ` / `--scope=` | comma-split, trimmed, filtered; repeated occurrences accumulate; the flag+value are spliced out of `args`. Value missing or starting with `--` → `stderr> Error: --scope requires a value. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`, `exit 1`. Unknown → `stderr> Error: unknown --scope value(s): ${unknown.join(', ')}. Valid scopes: ...\n`, `exit 1`. `RULE_SCOPES` = union of rules' `scopes` = `type`, `layout` (in insertion order: `type` first from `overused-font`, then `layout` from `nested-cards`). | | `--viewport ` / `--viewport=WxH` | regex `/^(\d{2,5})x(\d{2,5})$/i`; failure → `stderr> Error: --viewport requires a WxH value, e.g. --viewport 390x844\n`, `exit 1`. Sets `baseScanOptions.viewport = {width,height}` (browser scans only). Spliced out. | | `--cdp ` / `--cdp=` | Rust engine only. Scan a page of a running Chromium-based app through its DevTools endpoint instead of any target (see **Attached pages** below). Spliced out with its value (the last one given wins). Value missing (end of args, or the next arg starts with `--`) → `stderr> Error: --cdp requires a DevTools port or URL, e.g. --cdp 9222 or --cdp ws://127.0.0.1:9222/devtools/browser/\n`, `exit 1`. Accepted values: a port (`9222`, host `127.0.0.1`), `host:port`, `[v6]:port`, `http://host:port[/]`, `ws://host:port[/]`, `ws://host:port/devtools/browser/`, `ws://host:port/devtools/page/` (that page is the target). A host that is not `localhost` or a loopback address → `stderr> Error: --cdp only attaches to a DevTools port on this machine (127.0.0.1, localhost or [::1]), not \n`; a port outside 1 to 65535 → `stderr> Error: --cdp port must be between 1 and 65535; got \n`; another scheme → `stderr> Error: --cdp takes a port, host:port, http:// or ws:// URL; got \n`; another path → `stderr> Error: --cdp expects a DevTools port or a ws://.../devtools/browser/ or .../devtools/page/ URL; got \n`; each `exit 1`. With `--viewport` → `stderr> Error: --viewport does not apply with --cdp: the attached window keeps its own size\n`, `exit 1`. With positional targets (checked after `--help`) → `stderr> Error: --cdp scans the page of a running app and takes no file or URL targets\n`, `exit 1`. Stdin is never read under `--cdp`. | | `--target ` / `--target=` | With `--cdp`: picks the page (below). Spliced out with its value. Missing or blank value → `stderr> Error: --target requires a substring of the page's URL or title, e.g. --target localhost:5173\n`, `exit 1`. Without `--cdp` → `stderr> Error: --target picks a page of the app --cdp attaches to; pass --cdp with it\n`, `exit 1`. | | positional | `targets = args.filter(a => !a.startsWith('--'))` (so a bare `-x` single-dash arg is a *target*, except `-json`/`-fast` which were rewritten) | **Usage text** (`printUsage`, stdout, verbatim): ``` Usage: impeccable detect [options] [file-or-dir-or-url...] Scan files or URLs for UI anti-patterns and design quality issues. Options: --json Output results as JSON --quiet In text mode, only print the final findings count --github Write GitHub Actions annotations for each finding. Not with --json or --list-rules --list-rules Print the detector rule catalog and exit. --json for JSON, --quiet for ids only. Takes no targets --scope Only report rules in the given design domain (type, layout). Comma-separated. --viewport Browser viewport for URL scans (default 1280x800), e.g. --viewport 390x844 for a mobile-width pass --no-config Do not apply project config, detector ignores, inline ignore comments, or DESIGN.md --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments --no-design-system Do not load local DESIGN.md / .impeccable/design.json context --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse) --no-consent-hiding URL scans: keep known consent managers' banners (OneTrust, Cookiebot, ...) on the page instead of hiding them before the scan --no-overlay-hiding URL scans: keep product tours (driver.js) and preloaders still covering the page instead of hiding them before the scan --cdp Scan a page of a running Chromium-based desktop app (Electron, NW.js, CEF, WebView2) through its local DevTools port instead of loading a URL. The page is scanned as shown and left as found: never navigated, resized or closed. Takes no file or URL targets --target With --cdp: pick the page whose URL or title contains this text when the app has more than one --help Show this help message Advisory findings: Some rules are advisory: detected and listed in a separate section, but never counted as failures and never changing the exit code. They stay out of the failure count so they never block automation. --no-advisory hides them. 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. --github writes Actions annotations to stdout (no human findings). --list-rules writes the catalog to stdout (no scan). 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, and detector.designSystem.enabled. Inline ignores: In-file comments waive a finding where it lives and travel with the file: .brand { font-family: Inter } /* impeccable-disable-line overused-font */ // impeccable-disable-next-line bounce-easing: intentional bounce impeccable-disable applies to the whole file; -line / -next-line are scoped. List one or more rule ids (comma-separated), or omit them / use * for all. Detection modes: HTML files Static HTML/CSS analysis (default, catches linked CSS) Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.) URLs Puppeteer full browser rendering (auto-detected; http(s):// and file:// URLs; accessible linked CSS included) Running apps --cdp: a page of a Chromium-based app, attached through its DevTools port and scanned like a URL Examples: impeccable detect src/ impeccable detect index.html impeccable detect https://example.com impeccable detect --cdp 9222 --target "My App" --json impeccable detect --json . impeccable detect --github src/ impeccable detect --list-rules impeccable detect --no-config src/ ``` **Config**: `readDetectionConfig(process.cwd())` (see "Config file" below). Note config is resolved from **cwd**, not from the target. **Design system per target**: `scanOptionsFor(localPath)`: if design system enabled and `localPath` given, `loadDesignSystemForTarget(localPath, {cache})` walks up from the target's own dir (never cwd): a dir with `DESIGN.md`/`Design.md`/`design.md` (directly, or under fallback dirs `.agents/context`, `docs`) is the design root; a dir with a project marker (`.git`, `package.json`, `.impeccable`) but no DESIGN.md is a boundary → no design system; reaching `os.homedir()` or fs root → none. Sidecar candidates: `/.impeccable/design.json`, `/DESIGN.json`, `/DESIGN.json`. Cache key `root:` or `\0none`. Options become `{...baseScanOptions, designSystem}` when found. `design-system.mjs` has **no CLI surface** (no main guard, no argv parsing); it only exports functions. **A DESIGN.md purple switches the purple AI-palette check off** (Rust engine, corpus decision r3-23-ai-color-palette-brand-hue). When a target's design system declares a purple or violet colour (a DESIGN.md frontmatter `colors` entry or a sidecar colour, chroma at least 30 and hue 250 to 320 degrees: `is_declared_purple` in `crates/core/src/checks/rules.rs`), every engine drops the purple/violet forms of `ai-color-palette` for that target: the static HTML engine (`Purple/violet text (#hex) on heading`, `text-purple-* on heading`, `Purple/violet gradient (Tailwind)`, `Purple/violet accent colors detected`), the regex engine (`text-(purple|violet|indigo)-* on heading`, `from-(purple|violet|indigo)-* gradient`) and the URL engine on a `file:` target (the same forms plus `Purple/violet gradient background` and `Purple/violet neon text on dark background`). A form is purple when its snippet names purple, violet or indigo (`is_purple_palette_finding`); the cyan forms (`Cyan gradient background`, `Cyan neon text on dark background`) keep reporting, and so does every other rule. The drop runs after the built-in passes and before a rule pack and inline ignores, so the design hook, which scans through the same engines with the project's design system, and the wasm `detect` exports get it too. `http(s)` targets never load a design system (above) and are unaffected. In text mode (not `--json`, `--quiet` or `--github`) the CLI then prints, once per design system that applied to a scanned target and after the findings or alone when there are none: `stderr> \nNote: ai-color-palette's purple/violet check is off: ${source_path} declares ${label} (${hex}).\n` (no leading blank line when there are no findings; `${label}` is the first purple's declared name, `colors.primary` or `sidecar.`). JSON output has no place for notes and is unchanged apart from the dropped findings. **Input resolution**: 1. If `!process.stdin.isTTY && targets.length === 0` → **stdin mode** (`handleStdin`): read all stdin; try `JSON.parse`; if `parsed.tool_input.file_path` exists on disk → `detectLocalFile(fp, scanOptionsFor(fp))` (hook payload dispatch); else `detectText(input, '', scanOptionsFor(null))`. 2. Else `paths = targets.length ? targets : [process.cwd()]`. `urlRe = /^(?:https?|file):\/\//i`. If more than one URL target, a shared browser is created once via `createBrowserDetector()` (defaults `waitUntil:'load'`, `settleMs:100`), closed in `finally`; a single URL uses `detectUrl` directly (`waitUntil:'networkidle0'`, `settleMs:0`). 3. For each target, in order: - URL: `file:` URLs get `scanOptionsFor(fileURLToPath(url) or null)`; http(s) get `baseScanOptions` (never cwd's design system). Errors: `stderr> Error: ${e.message}\n`, continue. - Else `resolved = path.resolve(target)`; `fs.statSync` failure → `stderr> Warning: cannot access ${target}\n`, continue. - **Directory**: unless `--json`/`--quiet`/`--github`, `detectFrameworkConfig(resolved)` (see below) and if found probes the port, writing one of three stderr notices: - listening & matched: `\n${name} dev server detected on localhost:${port}.\nFor more accurate results, scan the running site:\n npx impeccable detect http://localhost:${port}\n\n` - listening & !matched: `\n${name} project detected (${basename(configPath)}).\nPort ${port} is in use by another service. Start the ${name} dev server and scan via URL for best results.\n\n` - not listening: `\n${name} project detected (${basename(configPath)}).\nStart the dev server and scan via URL for best results:\n npx impeccable detect http://localhost:${port}\n\n` Then `files = walkDir(resolved, config.extensions).filter(f => !shouldIgnoreDetectionFile(f, cwd, config))`. The walk collects built-in scannable extensions plus any `detector.extensions` from cwd config (empty under `--no-config`). If `files.length > 50 && stdin.isTTY && !json && !quiet && !github`: `stderr> \nFound ${n} files (${htmlCount} HTML) in ${target}.\nScanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\nTarget a specific subdirectory to narrow scope.\n` then readline prompt `Continue? [Y/n] ` on stderr; empty or `/^y(es)?$/i` continues; otherwise `stderr> Aborted.\n`, `exit 0`. Then `buildImportGraph(files)` → reverse map; each file scanned with its own options; findings from a file that is imported get `f.importedBy = [basename(importer), ...]` (Set iteration order). - **File**: skipped if `shouldIgnoreDetectionFile`; else `detectLocalFile`. - `detectLocalFile(fp, opts)`: a configured `detector.extensions` entry matching the file (longest suffix) picks the engine (`html` → `detectHtml(fp, opts)`, `text` → `detectText`); otherwise extension (lowercased) in `HTML_EXTENSIONS = {'.html','.htm'}` → `detectHtml(fp, opts)`; else `detectText(readFileSync(fp,'utf-8'), fp, opts)`. The design hook uses the same rule. - **Linked stylesheets (#1087)**: the static HTML engine reads every local `` for the cascade, but a linked sheet that `detector.ignoreFiles` waives is left out of the page-level pattern scan, so a pattern found only in that sheet is no longer reported under the page at line 0. Element rules still apply its styles through the cascade, and sheets that are not waived join the pattern scan as before. The design hook waives linked sheets the same way, matching each one the way it matches the edited file. - **Regex-only note (#884)**: after every target is scanned (never in stdin mode), unless `--json`/`--quiet`/`--github`, if `n > 0` local files went to `detectText` (by the `detectLocalFile` routing above, so a configured `html`-engine extension is not counted): `stderr> \n${n} file${n === 1 ? '' : 's'} scanned with regex matching only${exts.length ? ` (${exts.join(', ')})` : ''}.\nRules that need a parsed page (tiny-text, low-contrast and other element rules) did not run on them; findings are an undercount, not a clean bill of health.\n` where `exts` is the sorted set of those files' lowercased extensions (`extFromFilePath`, empty ones dropped). Then the full scan: `ports` = the sorted, deduplicated `port` of every scanned directory that matched `FRAMEWORK_CONFIGS` (whether or not the dev-server nudge found it listening); when non-empty, `For the full check, scan ${ports.length === 1 ? 'the running app' : 'each running app'}:\n` followed by ` npx impeccable detect http://localhost:${port}\n` per port, else `For the full check, scan the running dev server (npx impeccable detect http://localhost:PORT) or a static site's built .html.\n`. Then, only when `exts` intersects `HTML_LIKE_TEMPLATE_EXTS = ['.astro', '.svelte', '.vue']` (in that order, `t` = the intersection): `${t.join('/')} files can take the HTML engine instead: add { "ext": "${t[0]}", "engine": "html" } under detector.extensions in .impeccable/config.json (trades the regex rules and line numbers for the element rules).\n`. JSX/TSX get no opt-in line because the HTML parser reads nothing useful from them. The note precedes the findings output and changes neither stdout nor the exit code. 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 || 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 `), an unreadable directory or file in a dir walk, and a per-file scan that throws (`stderr> Error: cannot scan : `). A multi-URL scan whose shared browser fails to launch prints its `Error:` once and skips every URL target. A URL whose loaded page is a bot challenge or an HTTP error page is also a failure to scan (see **Page validity gate** under URL scans): it reports no findings and prints `stderr> Error: the page is ...`. **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(hadOperationalFailure ? 1 : (primary.length > 0 ? 2 : 0))`. - no findings: json → `stdout> []\n`; text/quiet → nothing. `exit(hadOperationalFailure ? 1 : 0)`. - Text mode (not `--json`, `--quiet` or `--github`) prints scan notes after the findings, or alone when there are none, one line each in first-seen order: the DESIGN.md purple note; for a URL scan that hid consent banners `Hid the consent banner(s) on before scanning. Pass --no-consent-hiding to scan it.` (`` joined `A, B and C`); for one that hid tours or preloaders `Hid on before scanning. Pass --no-overlay-hiding to scan it.`, each item `the tour`, `the badge`, `the preloader `, `the intro ` or `the splash `, joined the same way (a site's own cookie notice is named `cookie notice` among the consent banners); and for a URL scan whose capture held sliders that never started, the capture note under `content-hidden-at-rest` below. - 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. **Text format** (`formatFindings(findings, false)`): ``` formatFindingsBody(primary): group by f.file preserving first-seen order; for each file: "\n${file}${importNote}" where importNote = items[0].importedBy?.length ? ` (imported by ${items[0].importedBy.join(', ')})` : '' for each item: " ${item.line ? `line ${item.line}: ` : ''}[${item.antipattern}] ${item.snippet}" " → ${item.description}" (U+2192 arrow, 4-space indent) then "\n${primary.length} anti-pattern${1?'':'s'} found." then, if advisory non-empty: "\n" + dim('── Advisory (not counted as failures) ──') (U+2500 box-drawing dashes) each body line individually dim()-wrapped dim(`\n${advisory.length} advisory note${1?'':'s'}. Suppress with --no-advisory.`) all joined with '\n'. ``` Example (non-TTY): ``` /abs/a.css line 3: [side-tab] .card — border-left: 4px solid #6366f1 → Thick colored border on one side of a card — ... 1 anti-pattern found. ── Advisory (not counted as failures) ── /abs/a.html [em-dash-overuse] 9 em-dashes in body text → Em-dash saturation ... 1 advisory note. Suppress with --no-advisory. ``` **Finding object** (`cli/engine/findings.mjs` `finding(id, filePath, snippet, line = 0)`), key order exactly: ```js { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', category: ap.category || null, file: filePath, line, snippet } // plus, only when the effective severity is 'advisory': advisory: true ``` Optional keys added later by engines (appended after the above): `ignoreValue` (design-system rules; browser findings with a value), `importedBy` (dir scans), `selector`, `thirdParty`, `consentHidden` and `overlaysHidden` (URL scans, below), `severity` may be overwritten by per-finding promotion (browser & html-patterns, e.g. pulsing dot in a header). Design-system findings are `{...finding(...), ...extras}` where extras = `{ ignoreValue }`. Static-HTML and browser findings have `line: 0`; regex findings have 1-based lines. `severity` values in registry: `'warning'` (default), `'advisory'` (many generated-UI tells and design-system-color/radius/font-size, numbered-section-labels, blinking-cursor, shape-assembled-illustration, bounce-easing, straggler, unbalanced-heading, hard-offset-shadow), `'error'` (`script-error`, `content-hidden-at-rest`). Per-finding demotions to `'advisory'` in the Rust engine: `flat-type-hierarchy` when weight separates the roles (below), a `script-error` an ad-tech script threw, and a `script-error` that is one of React's recoverable hydration errors (URL scans). `severity` is the canonical advisory field (#709): `deriveAdvisoryFlag` stamps `advisory: true` when and only when the effective severity is `'advisory'`, so a per-finding promotion or demotion carries the flag with it, and every `severity:'advisory'` rule is partitioned out of the failure count and the exit code. `isAdvisory` accepts either `finding.advisory === true` or `finding.severity === 'advisory'`. **hard-offset-shadow** (advisory, `quality`, the static HTML engine and the URL engine; the regex engine that scans CSS, JSX, TSX and other source files does not run it, so a quiet source scan has not checked shadows): reads the element's resolved `box-shadow` with its `color` (for `currentColor`) and effective opacity, and reports once per element when every layer of the list parses and one outer layer has zero blur, a non-negative spread, an x or y offset of at least 4px and a painted alpha (layer alpha times opacity) above 0.1. A zero length counts as zero in any length unit (`0em` blur is zero blur); a non-zero length in a unit other than `px` is unresolved. An unparseable layer anywhere in the list, an `inset` layer, or an unresolved length or colour reports nothing. The static engine skips an element that is `hidden`, `display: none` (on it or an ancestor) or `visibility: hidden`/`collapse`. An empty or `none` shadow returns before the list is tokenized or any ancestor is walked. Waived with `data-impeccable-ignore="hard-offset-shadow"`. **Categories**: `category` is `'slop'` (AI tells) or `'quality'`. Category has **no effect on output**, ordering, or exit codes; it is only carried in the finding and used by `getRulesForCategory`. Registry (62 ids, in order): side-tab, border-accent-on-rounded, overused-font, flat-type-hierarchy, gradient-text, ai-color-palette, cream-palette, nested-cards, monotonous-spacing, bounce-easing, pulsing-dot, blinking-cursor, shape-assembled-illustration, organic-clip-path, buried-raster, hard-offset-shadow, dark-glow, radial-halo, radial-spotlight-glow, marquee, icon-tile-stack, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, em-dash-overuse, marketing-buzzword, aphoristic-cadence, oversized-h1, extreme-negative-tracking, broken-image, script-error, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, gray-on-color, low-contrast, line-length, cramped-padding, body-text-viewport-edge, tight-leading, skipped-heading, heading-rhythm, justified-text, straggler, unbalanced-heading, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, text-overflow, repeated-container-text, clipped-overflow-container, design-system-font, design-system-color, design-system-radius, design-system-font-size, gpt-thin-border-wide-shadow, repeating-stripes-gradient, codex-grid-background, theater-slop-phrase. Retired ids (`image-hover-transform`, `layout-transition`) are in no list and no engine reports them; an `ignoreRules` entry or an inline `impeccable-disable` naming one still parses and suppresses nothing, and `doctor` reports it as `detector-ignore-rules-unknown`. Scopes: `type` = overused-font, flat-type-hierarchy, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, oversized-h1, extreme-negative-tracking, line-length, tight-leading, skipped-heading, heading-rhythm, justified-text, straggler, unbalanced-heading, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, design-system-font, design-system-font-size; `layout` = nested-cards, monotonous-spacing, icon-tile-stack, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, line-length, cramped-padding, body-text-viewport-edge, heading-rhythm, text-overflow, clipped-overflow-container. `RULE_ENGINE_SUPPORT = { regex: Set['source','page-analyzer'], 'static-html': Set['element','page'], browser: Set['element','page','layout'], visual: Set['visual-contrast'] }`. **Tests**: `tests/detect-antipatterns-fixtures.test.mjs` (CLI block: `--help exits 0` and contains `Usage:`/`--quiet`, not `--gpt`; `--gpt` prints deprecation; `detect` prefix accepted; should-pass exits 0; should-flag exits 2 with `side-tab` in stderr; `--json` parses; `--quiet` stdout empty and stderr matches `/^[1-9]\d* anti-patterns? found\.$/`; `formatFindings — advisory partitioning`), `tests/detect-cli-stdin-dispatch.test.mjs`, `tests/detect-cli-design-contamination.test.mjs`, `tests/inline-ignores.test.mjs` ("detect CLI end-to-end"), `tests/detect-url-launch.test.mjs`. #### `cli/engine/node/file-system.mjs` - `SKIP_DIRS = {'node_modules','dist','build','__pycache__'}`; any directory whose name starts with `.` is skipped **except** `HIDDEN_SOURCE_DIRS = {'.vitepress','.vuepress','.storybook'}`. The root passed to `walkDir` is never name-checked (an explicit hidden dir scans). - `SCANNABLE_EXTENSIONS = {'.html','.htm','.css','.scss','.sass','.less','.jsx','.tsx','.js','.ts','.mjs','.cjs','.vue','.svelte','.astro','.blade.php'}`; `hasScannableExtension` lowercases and also matches multi-dot exts by `endsWith` (`.blade.php`). CLI directory walks pass configured `detector.extensions` as a second suffix match: `name` lowercased; `name.length > ext.length && name.endsWith(ext)`. - `walkDir` returns files in `readdirSync` order, recursive, unreadable dirs → `[]`. - **There is no generated-file detection in the CLI** (`skill/scripts/lib/is-generated.mjs` is hook-side only and not imported by `cli/`). - Import graph: `IMPORT_SPECIFIER_PATTERNS = [/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, /@(?:use|forward)\s+['"]([^'"]+)['"]/g]`; `resolveImport` only for specifiers matching `/^[./]/`: exact, `base+ext` for each scannable ext, then `base/index+ext`. - `FRAMEWORK_CONFIGS` (first match wins, in this order): Next.js (`next.config.js|mjs|ts`, 3000, `/port\s*[:=]\s*(\d+)/`, header `x-powered-by` ~ `/next/i`); SvelteKit (`svelte.config.js|ts`, 5173, header `x-sveltekit-page` any); Nuxt (`nuxt.config.js|ts`, 3000, `x-powered-by` ~ `/nuxt/i`); Vite (`vite.config.js|ts|mjs`, 5173, body `/@vite\/client/`); Astro (`astro.config.js|ts|mjs`, 4321, body `/astro/i`); Angular (`angular.json`, 4200, `/"port"\s*:\s*(\d+)/`, body `/ng-version/i`); Remix (`remix.config.js|ts`, 3000, `x-powered-by` ~ `/remix/i`). Port overridden by first regex match in the config file. - `isPortListening(port, fingerprint)`: with fingerprint → `fetch('http://localhost:${port}/')` 2s abort, header check then body check → `{listening:true, matched:bool}`; error → `{listening:false}`. Without fingerprint → TCP connect to 127.0.0.1 with 500ms timeout. #### `cli/engine/shared/inline-ignores.mjs` — inline ignore comments - `DIRECTIVE_RE = /impeccable-(disable-next-line|disable-line|disable)\b[ \t]*([^\n\r]*)/gi` (matched anywhere on a line, any comment syntax; case-insensitive). - `TRAILING_CLOSER_RE = /\s*(?:\*\/\}?|--+>|\*\}|#\}|%>|\}\})\s*$/` stripped from the remainder; then reason cut at first `/\s*(?:--+|:)\s*/`; tokens split on `/[\s,]+/`, lowercased; empty or containing `*` → `['*']`. - Lines split on `'\n'` only (CRLF-safe since `\r` excluded from capture). `disable` → file set; `disable-line` on line i (1-based) → `line.get(i)`; `disable-next-line` on line i → `nextLine.get(i+1)`. - `isInlineIgnored(finding)`: rule = lowercased `finding.antipattern`; file set match (`*` or rule) → true; if `line > 0`, `line`/`nextLine` set match → true. Line-less findings (static HTML, browser) match **only** whole-file directives. - Applied inside `detectText` and `detectHtml` at the end unless `options.inlineIgnores === false` (set by `--no-config` or `--no-inline-ignores`). Not applied to URL scans. - Fast path: skip unless `/impeccable-disable/i` occurs. - **DOM-scoped ignore** (`rules/checks.mjs scopedIgnoreActive`): attribute `data-impeccable-ignore="rule-a rule-b"` (split on `/[\s,]+/`, lowercased; empty value or `*` = all) on an element waives matching findings for it and its subtree in browser, extension, and static engines. In `detectHtml`'s html-patterns pass, selector-backed findings are dropped when every element matched by the (pseudo-stripped) selector is under a waiver; unmatched selectors keep the finding. - Tests: `tests/inline-ignores.test.mjs`; fixture `scoped-ignore.html`. #### Config file (`cli/lib/impeccable-config.mjs`) — `.impeccable/config.json` + `.impeccable/config.local.json` - Paths: `/.impeccable/config.json` (shared) and `/.impeccable/config.local.json` (per-dev; `writeDetectionConfig(...,{local:true})` also appends a block to `.git/info/exclude`: `# impeccable-config-ignore-start\n.impeccable/config.local.json\n# impeccable-config-ignore-end`, idempotent by marker regex, following `gitdir:` files, and a linked worktree's `commondir`, to the exclude file git reads). - Shape: ```json { "detector": { "ignoreRules": ["side-tab"], "ignoreFiles": ["src/legacy/**"], "ignoreValues": [{ "rule": "overused-font", "value": "inter", "files": ["src/a.css"], "createdAt": "ISO", "reason": "..." }], "designSystem": { "enabled": true }, "advisoryRules": "include"|"exclude" }, "hook": { "consent": "accepted"|"declined", ... }, "updateCheck": true } ``` - `readDetectionConfig(root)`: start `{ignoreRules:[],ignoreFiles:[],ignoreValues:[],designSystem:{enabled:true},extensions:[]}`; for shared then local: apply legacy `raw.hook.*` section then `raw.detector.*`. Arrays are unioned (`uniqueStrings`, String-coerced); ignoreValues merged by key `rule\0value\0sortedFiles.join('\x1f')` (later wins); `designSystem.enabled` false only when literally `false`; `advisoryRules` copied only if `'include'|'exclude'`; `extensions` merges normalized `detector.extensions` entries by ext, later source wins (string → engine `html`; `{ext, engine}` → `text` only when engine is literally `"text"`). Invalid/non-array `extensions` skipped. Invalid JSON / non-object files are ignored silently. **No validation errors are ever raised by the CLI**; the only validation of ignore lists lives in `skill/scripts/lib/staleness-deep.mjs checkDetectorIgnores` (doctor): unknown `ignoreRules` ids vs live `ANTIPATTERNS` → finding `detector-ignore-rules-unknown` (severity `mention`); non-glob `ignoreFiles` entries that don't exist → `detector-ignore-files-missing`. - `normalizeIgnoreValue(v)`: trim, strip one leading/trailing quote, `+`→space, collapse whitespace, lowercase. Rules lowercased/trimmed. - `normalizeIgnoreValueEntries`: keeps `{rule, value, [files], [createdAt], [reason]}` in **that key order**; `file` (string) and `files` merged, trimmed, deduped. - Glob → regex: `**/` → `(?:.*/)?` (zero or more whole directories, so `src/**/a.html` matches `src/a.html` and `src/pages/a.html` but not `src/xa.html`), any other `**` → `.*`, `*` → `[^/]*`, `?` → `[^/]`, `{a,b}` → `(?:a|b)`, regex specials escaped; anchored `^...$`. `matchesAnyGlob` tests the `/`-normalized path and its basename. - `shouldIgnoreDetectionFile(filePath, root, config)`: raw path, absolute path, and root-relative path (if inside root) tested against `ignoreFiles`. - `filterDetectionFindings`: drop when `ignoreRules` has the rule, or an `ignoreValues` entry matches: same rule; entry.value `*` (wildcard) OR extracted value equals (with color-key equality for `design-system-color`: rgb/hex/hsl parsed to `r,g,b,round(a*255)`); if entry has `files`, `finding.file` (or any `/`-suffix of it) must glob-match; a wildcard with no files never matches (unscoped `*` disallowed). - `extractFindingIgnoreValue`: only for `overused-font, bounce-easing, design-system-font, design-system-color, design-system-radius, design-system-font-size`; source `finding.ignoreValue || finding.value`, else parse `detail`/`snippet`: bounce → `animate-bounce`, `cubic-bezier(...)`, or animation token matching `/bounce|elastic|wobble|jiggle|spring/i`; fonts → `Primary font:`, `Google Fonts:`, `font-family:` value, or `family=` URL param (decoded). #### `impeccable ignores` (`cli/bin/commands/ignores.mjs`) - Actions/aliases: `status|ls|list`→list (default when no action), `add-rule|ignore-rule`, `add-file|ignore-file`, `add-value|ignore-value|update-value`, `remove-rule|rm-rule`, `remove-file|rm-file`, `remove-value|rm-value`, `clear`. `--help`/`-h` prints usage (stdout). Unknown → throws `Unknown ignores action: ${a}. Run "impeccable ignores --help".` (exit 1 via cli.js). - Scope flags: `--shared` (default), `--local`, `--all` (remove/clear only); more than one → error `Pass only one scope flag: --shared, --local, or --all` (or `--shared or --local`). - `add-rule [--all-values] [--reason ...]`: `overused-font` without `--all-values` → error "overused-font is value-specific by default. Use add-value overused-font , or add-rule overused-font --all-values for broad suppression." Output: `Added ${rule} to ${local?'local':'shared'} detector ignoreRules (${relpath}).` - `add-file ` → `Added ${glob} to ... detector ignoreFiles (...)`. - `add-value [--file ]... [--reason ]`: value = normalized join of positionals after rule; `--file`/`--files`/`--file=`/`--files=` (empty or flag-like → error); unknown `--x` → `Unknown add-value flag: --x`; `*` value requires `--file`; existing entry (same key) updates reason/files, else pushes `{rule,value,[files],createdAt: ISO now,[reason]}`. Output `Added ${rule}=${value} to ... detector ignoreValues (...)`. - `remove-*` → `Removed ${n} from shared (path), ${n} from local (path).` or `No matching detector ignore found.` `clear` → `Cleared detector ignores in ${'shared and local config'|'local config'|'shared config'}.` - `list` output: ``` Impeccable detector ignores shared file: .impeccable/config.json local file: .impeccable/config.local.json Merged: ignoreRules: (none) ignoreFiles: ... ignoreValues: rule=value [glob1, glob2] - reason, ... designSystem: enabled|disabled Shared: ... Local: ... ``` - Tests: `tests/cli-ignores.test.js`. #### `cli/engine/engines/browser/detect-url.mjs` — URL scans - `detectUrl(url, options)`; options: `profile`, `waitUntil` (default `'networkidle0'`), `settleMs` (default 0), `viewport` (default `{width:1280,height:800}`), `browser` (external), `headless` (default true), `designSystem`, `scriptErrors` (default on), `contentHidden` (default on), `visualContrast`/`visualContrastBrowser`/`visualContrastPixel` (default on), `visualContrastMaxCandidates` (12), `visualContrastScrollOffscreen` (true). - Puppeteer imported dynamically; missing → throw `puppeteer is required for URL scanning. Install: npm install puppeteer`. Browser script read from `/detect-antipatterns-browser.js`; missing → `Browser script not found at ${path}`. - Launch: `launchArgs = process.env.CI ? ['--no-sandbox','--disable-setuid-sandbox'] : []`. On `win32` first `puppeteer.launch({channel:'chrome', headless, args})`, on failure fall back to bundled `puppeteer.launch({headless, args})` with `err.cause = channelError`. Non-Windows: bundled only. - Flow: `newPage` → attach `pageerror` listener (message first line, trimmed, sliced to 160 chars, deduped) → `setViewport(viewport)` → `page.goto(url, {waitUntil, timeout: 30000})` → optional settle → `page.evaluate` sets `window.__IMPECCABLE_CONFIG__ = {...existing, autoScan:false, ...(designSystem ? {designSystem} : {})}` where designSystem serialized as `{present:true, hasFonts, allowedFonts:[...], hasColors, allowedColors:[{r,g,b}], hasRadii, allowedRadii:[px], hasPillRadius}` → `page.evaluate(browserScript)` (injects the bundle; defines `window.impeccableDetect/impeccableDetectAsync/impeccableScan/impeccableScanAsync/impeccableMeasureHiddenText/impeccableCollectVisualContrastCandidates/impeccableAnalyzeVisualContrast/impeccableGetLastVisualContrastAnalyses`) → `window.impeccableDetect({decorate:false, serialize:true})` returns groups `[{selector, tagName, rect, isPageLevel, isHidden, findings:[{type, category, severity, advisory, detail, ignoreValue, name, description}]}]`; flattened to `{id:type, snippet:detail, ignoreValue, severity}` → content-hidden sweep (`measureContentHiddenAfterReveal`: scroll in steps `max(200, floor(innerHeight*0.7))` with `behavior:'instant'`, 40ms rAF pauses, back to top, wait 700ms, then `impeccableMeasureHiddenText()`; `checkContentHiddenAtRest` fires when `totalChars>=200 && hiddenChars>=150 && share>0.3`, snippet `${pct}% of page text (${hidden} of ${total} chars) stays at opacity 0 / visibility hidden after reveal handlers ran (e.g. "sample")`) → up to 3 `script-error` findings `{id:'script-error', snippet:message}` → visual contrast fallback → `finally` close page (and browser if owned). - Visual contrast fallback (`runVisualContrastFallback`): browser-side `impeccableAnalyzeVisualContrast({maxCandidates, scrollOffscreen})` findings not already low-contrast on that selector; then candidates (from analyses or `impeccableCollectVisualContrastCandidates`) not resolved pass/fail → per candidate `captureVisualContrastCandidate(page, candidate, viewport)`. - **Screenshot flow** (`screenshot-contrast.mjs`, as the JS ran it; the Rust engine's verdict and refusals replace the p10 rule below, see **What the visual pass refuses to read** under the Rust engine additions): clip sanitized (`x,y` floored ≥0; `width` ≤ viewport width (default 1600), `height` ≤ 320, both ≥1); `page.screenshot({encoding:'base64', clip, captureBeyondViewport:true})` before; inject `; Astro:

…

exactly ONE top-level element, same tag as original
…
…
``` Param kinds: `range` `{id,kind:'range',min,max,step,default,label}` → CSS var `--p-`; `steps` `{id,kind:'steps',default,label,options:[{value,label}]}` → attribute `data-p-=""`; `toggle` `{id,kind:'toggle',default:boolean,label}` → `--p-: 1|0` and attribute `data-p-="on"` present only when on. Browser drives range/toggle vars through an injected stylesheet (`#impeccable-live-variant-state`), and hides non-visible variants there; values reset to declared defaults on variant switch. Budget/authoring rules (0-4 per variant, hard cap 4) are prose in live.md section 7. Optional readiness sentinel `--impeccable-variant-ready` (stripped on accept). `cssAuthoring` object returned by wrap/insert: - scoped: `{mode:'scoped', styleTag:' (only when paramValues non-empty)
…variant lines…
``` - Astro (`.astro`): as HTML, except the style tag is the file type's own `styleTag`, ``, `{/* … */}` comments, `style={{ display: 'contents' }}` on the variant div. Result `{handled:true, file: rel, carbonize:boolean, todo?:'REQUIRED before next poll: carbonize cleanup in . See reference/live.md "Required after accept".', bakeSkipped?}`. Discard: replace range with deindented original → `{handled:true, file, carbonize:false}`; refused → `{handled:false, file, carbonize:false, error}`. **Mechanical bake** (`bake.rs`): only on `--bake` (never by default and never on `--no-bake`; the generate lane's accept carbonizes exactly like plain live's, so the agent integrates the accepted variant per live.md), a knob-free HTML/JSX accept is made permanent instead of leaving the carbonize block. Refused (falls back to the carbonize block, with `bakeSkipped:`) when: `--param-values` is non-empty; the accepted variant carries `data-impeccable-*` or `data-p-*` inside it; the preview CSS uses `var(--p-*)`, `data-p-*`, or `data-impeccable-params`; the variant's root is a component (``, ``: what it renders is unknown, and its `className` or `id` prop may never reach that element) or has neither an id nor a static class (`className={expr}`); the anchor cannot be shown to match the accepted element alone (the journaled generate event's `element.anchor` must be the same selector, the id or the tag and class set, and its `element.anchorMatches` must be 1: ` matches N elements on the page; a lasting rule on it would restyle them all`, a different anchor, and a missing descriptor or count each name themselves); a `:scope` cannot be rewritten (sibling combinators, `:scope` not at the front, nested `@scope`); the accepted variant declares no rule; or no destination stylesheet exists. The rewrite: the accepted `@scope ([data-impeccable-variant="N"])` block is flattened and every selector re-anchored on the root tag's selector (`#id`, else `tag.class.class`): `:scope > .x` → `` merged with `.x` (the wrapper's only child is the element itself, so the child compound rides on the anchor, a class the anchor already has once, a type only when it is the anchor's own; `#id` anchors take any type), `:scope .x` → ` .x`, `:scope:hover > .x` → `.x:hover`, bare `:scope` → ``; Astro's `[data-impeccable-variant="N"] > .x` prefix the same way; nested `@media`/`@supports` inside the block keep their prelude; top-level `@keyframes`/`@font-face` are kept, other variants' blocks dropped. Destination: for `.jsx`/`.tsx` the `.css` file under the app root (skipping node_modules/.git/.impeccable/dist/build/coverage/framework caches, depth ≤ 6, `.min.css` and generated or git-ignored files excluded) with the most rules naming the anchor's id or classes, else the only `.css` file; for other files the page's own last `