# 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`. 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 | | `--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 | | `--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. | | 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 --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) --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. 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) Examples: impeccable detect src/ impeccable detect index.html impeccable detect https://example.com impeccable detect --json . 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. **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`, `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).filter(f => !shouldIgnoreDetectionFile(f, cwd, config))`. If `files.length > 50 && stdin.isTTY && !json && !quiet`: `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)`: 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 || 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. **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)`. - 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), `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), `'error'` (`script-error`, `content-hidden-at-rest`). `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'`. **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 (59 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, 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, layout-transition, line-length, cramped-padding, body-text-viewport-edge, tight-leading, skipped-heading, heading-rhythm, justified-text, 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, image-hover-transform. 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, 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','.vue','.svelte','.astro','.blade.php'}`; `hasScannableExtension` lowercases and also matches multi-dot exts by `endsWith` (`.blade.php`). - `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 for worktrees). - 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}}`; 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'`. 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: `**` → `.*` (swallowing a following `/`), `*` → `[^/]*`, `?` → `[^/]`, `{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`): 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…
``` - JSX: everything above wrapped in `
` … `
` with body indented 2 more, ``, `{/* … */}` 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}`. **Mechanical bake** (`bake.rs`): for a session whose snapshot carries `origin:'agent'` (a Go fired by `live-generate`) or on `--bake` (never on `--no-bake`; plain live sessions are untouched), 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}`); 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` → `.x`, `:scope .x` → ` .x`, `:scope:hover > .x` → `:hover > .x`, 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 `