From 0071508f7076816af96ae31675f8bdcb3f954031 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 17 Aug 2026 21:31:47 -0700 Subject: [PATCH] Docs: describe the launcher, the engine pin, and the oracle gate CLAUDE.md gains an Engine binary section (launcher lookup order, ENGINE_VERSION, untracked binaries, how tests get one, the oracle as behavior gate, what stays JavaScript) and drops the Node-script and JS-detector descriptions; the CLI and detection-rule sections point at the shim and the engine repo. README.md states the skill needs no runtime and lists the launcher-based hook commands; AGENTS.md follows. CLI-CONTRACT.md's intro notes the scripts it quotes are the recorded source, not the tree. Prepared with AI assistance (Claude Code). --- AGENTS.md | 33 ++++------- CLAUDE.md | 131 ++++++++++++++++++++----------------------- README.md | 24 ++++---- docs/CLI-CONTRACT.md | 10 +++- 4 files changed, 93 insertions(+), 105 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 39bcc730c..ba7e687e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization -`skill/` is the source of truth for the Impeccable skill: `SKILL.src.md`, `reference/`, `scripts/`, and `agents/`. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. The CLI and anti-pattern detector live in `cli/`, the browser extension in `extension/`, the Astro website in `site/`, Cloudflare Pages Functions in `functions/`, and regression coverage in `tests/` with fixtures under `tests/fixtures/`. `dist/` and `build/` are generated and gitignored. The root harness folders (`.agents/`, `.claude/`, `.cursor/`, etc.) and `plugin/` are generated distribution artifacts that are tracked for direct repo installs, not hand-authored source. +`skill/` is the source of truth for the Impeccable skill: `SKILL.src.md`, `reference/`, `scripts/`, and `agents/`. `skill/scripts/` holds the launcher (`impeccable`, `impeccable.cmd`), the pinned engine `VERSION`, `command-metadata.json`, and the in-page live-mode JS; every skill verb (`{{scripts_path}}/impeccable `) runs in the engine binary, which is built in a separate repo and pinned by the root `ENGINE_VERSION` file. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. `cli/` is the npm shim that runs the same binary, the browser extension lives in `extension/`, and regression coverage in `tests/` with fixtures under `tests/fixtures/` and the behavior goldens under `tests/oracle/`. `dist/` and `build/` are generated and gitignored. The root harness folders (`.agents/`, `.claude/`, `.cursor/`, etc.) and `plugin/` are generated distribution artifacts that are tracked for direct repo installs, not hand-authored source. ## Build, Test, and Development Commands @@ -12,11 +12,12 @@ - `bun run rebuild` - clean and rebuild everything from scratch without syncing tracked harness folders. - `bun run rebuild:release` - clean and rebuild everything, including tracked harness output sync. - `bun test tests/build.test.js` - run a focused Bun test. -- `bun run test` - run the full Bun + Node test suite (includes the plugin loader E2E, which installs the committed `plugin/` subtree into a sandboxed real Claude Code and skips cleanly when the `claude` CLI is absent). +- `bun run fetch:engine` - download the pinned engine binary for this machine into `skill/scripts/bin/-/` (or set `IMPECCABLE_BIN` to a local build). The oracle and framework suites skip without it. +- `bun run test` - run the full Bun + Node test suite (includes the oracle replay against the engine binary and the plugin loader E2E, which installs the committed `plugin/` subtree into a sandboxed real Claude Code and skips cleanly when the `claude` CLI is absent). - `bun run test:live-e2e` - opt-in live-mode E2E against framework fixtures (~2 min; needs `npx playwright install chromium` once). - `bun run test:skill-behavior` - opt-in LLM-backed checks that the SKILL.md Setup flow actually drives the agent (runs claude-sonnet-5 / gpt-5.6-luna / gemini-3.5-flash / deepseek-v4-flash; needs `.env` with provider keys). - `bun run test:plugin-e2e` - just the plugin loader E2E, for fast iteration on `plugin/`, `skill/agents/`, or `scripts/build.js` changes. -- `bun run build:browser` / `bun run build:extension` - rebuild browser-specific bundles. +- `bun run build:extension` - rebuild the extension bundle (`build:browser` is a no-op stub; the in-page bundle comes from the engine repo). Run `bun run build` after changing anything in `skill/`, transformer code, or user-facing counts. It validates the generated distribution under `dist/` without touching tracked root harness outputs. Use `bun run build:release` only when intentionally refreshing generated provider permutations for release/main-sync or build-system work. @@ -32,39 +33,27 @@ Some repo workflows need to run outside the sandbox in the desktop app: - GitHub SSH operations that depend on the 1Password SSH agent, such as `gh pr checkout`, may fail in the sandbox with `sign_and_send_pubkey` or no 1Password approval prompt. Rerun them outside the sandbox instead of falling back to unrelated workarounds. - `bun run build:release` rewrites committed harness directories such as `.agents/skills/`. In the sandbox, Bun can hit filesystem errors while removing/recreating those trees (for example `EFAULT` on `.agents/skills`). Rerun the release build outside the sandbox before treating it as a real build failure. -- Puppeteer/headless-Chrome tests, especially `node --test tests/detect-antipatterns-browser.test.mjs` and the browser portion of `bun run test`, can hang in the sandbox while launching Chrome. Run them outside the sandbox for authoritative results. -- The jsdom fixture suite is intentionally run with Node, not Bun: use `node --test tests/detect-antipatterns-fixtures.test.mjs` or the `bun run test` script. A direct `bun test tests/detect-antipatterns-fixtures.test.mjs` can time out and is not the supported signal. +- The oracle and framework suites spawn the engine binary many times; run them with Node (`node --test tests/oracle.test.mjs`), which is what `bun run test` does. ## Coding Style & Naming Conventions -Use ESM, semicolons, and the existing two-space indentation style in JS, HTML, and CSS. Prefer small, single-purpose modules over large abstractions. Keep filenames descriptive and lowercase with hyphens where needed; skill entrypoints stay as `SKILL.md`, helper scripts use `.js` or `.mjs`. In source frontmatter, use clear kebab-case names and concise descriptions. There is no dedicated formatter or linter configured here, so match surrounding code closely. +Use ESM, semicolons, and the existing two-space indentation style in JS, HTML, and CSS. Prefer small, single-purpose modules over large abstractions. Keep filenames descriptive and lowercase with hyphens where needed; skill entrypoints stay as `SKILL.md`, build and test helpers use `.js` or `.mjs`. In source frontmatter, use clear kebab-case names and concise descriptions. There is no dedicated formatter or linter configured here, so match surrounding code closely. ## Testing Guidelines Tests use Bun’s test runner plus Node’s built-in `--test`. Name tests `*.test.js` or `*.test.mjs` and place new fixtures near the behavior they cover, usually under `tests/fixtures/`. Prefer targeted test runs while iterating, then finish with `bun run test`. If you change generated outputs or provider transforms, verify both source parsing and at least one affected provider path in `dist/`. -For changes to `skill/scripts/live-*.{mjs,js}` or `skill/scripts/live/**`, also run `bun run test:live-e2e` (kept out of the default suite because it does real `npm install` per fixture and boots framework dev servers). Scope to one fixture with `IMPECCABLE_E2E_ONLY=` while iterating; pass `IMPECCABLE_E2E_DEBUG=1` for page-DOM and dev-server-log dumps on failure. Schema and authoring guide for new fixtures live in `tests/framework-fixtures/README.md`. +For changes to the live-mode page JS (`skill/scripts/live-browser*.js`) or an `ENGINE_VERSION` bump, also run `bun run test:live-e2e` (kept out of the default suite because it does real `npm install` per fixture and boots framework dev servers). Scope to one fixture with `IMPECCABLE_E2E_ONLY=` while iterating; pass `IMPECCABLE_E2E_DEBUG=1` for page-DOM and dev-server-log dumps on failure. Schema and authoring guide for new fixtures live in `tests/framework-fixtures/README.md`. Set `IMPECCABLE_E2E_AGENT=llm` to swap the deterministic fake agent for an API-backed one (`tests/live-e2e/agents/llm-agent.mjs`). Claude Haiku 4.5 is the primary path whenever `ANTHROPIC_API_KEY` is set. DeepSeek V4 Flash is the secondary cheap fallback when only `DEEPSEEK_API_KEY` is set, and can be forced with `IMPECCABLE_E2E_LLM_PROVIDER=deepseek` or `bun run test:live-e2e -- --llm-provider=deepseek`; override either model via `IMPECCABLE_E2E_LLM_MODEL` or `--llm-model=`. Tests skip cleanly when the selected provider key is unset. This path hits the API — use it for verification, not CI. -For changes to `skill/SKILL.src.md`'s Setup section, `skill/scripts/context.mjs`, or any Setup-touching reference file (`init.md`, `document.md`, `brand.md`, `product.md`, sub-command refs), also run `bun run test:skill-behavior`. The suite spawns current real models (claude-sonnet-5, gpt-5.6-luna, gemini-3.5-flash, deepseek-v4-flash) with the source SKILL.md inlined as system prompt and a workspace-scoped tool set, then asserts on the tool-call trace. Provider keys live in repo-root `.env`; missing keys skip cleanly. Scope to one provider with `IMPECCABLE_SKILL_BEHAVIOR_MODELS=`; add `IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1` to dump per-scenario traces. Baseline and per-scenario assertions live in `tests/skill-behavior/README.md`. +For changes to `skill/SKILL.src.md`'s Setup section or any Setup-touching reference file (`init.md`, `document.md`, `brand.md`, `product.md`, sub-command refs), also run `bun run test:skill-behavior`. The suite spawns current real models (claude-sonnet-5, gpt-5.6-luna, gemini-3.5-flash, deepseek-v4-flash) with the source SKILL.md inlined as system prompt and a workspace-scoped tool set, then asserts on the tool-call trace. Provider keys live in repo-root `.env`; missing keys skip cleanly. Scope to one provider with `IMPECCABLE_SKILL_BEHAVIOR_MODELS=`; add `IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1` to dump per-scenario traces. Baseline and per-scenario assertions live in `tests/skill-behavior/README.md`. -Other area-to-suite obligations (the canonical mapping is the `triggers` lists in `scripts/test-suites.mjs`; CLAUDE.md carries the full table): `serve-question.mjs` / `generate-image.mjs` / `concept-seed.mjs` changes owe `bun run test:new-work-e2e` (Playwright, offline); `cli/bin/commands/skills.mjs` changes owe `bun run test:cli-remote-e2e` (hits impeccable.style); accept/browser/server/wrap or SvelteKit adapter changes owe `bun run test:live-e2e-accept-cleanup` (provider-billed), and Svelte adapter/component changes owe `bun run test:live-svelte-adapter-deepseek` (DeepSeek-billed). +Other area-to-suite obligations (the canonical mapping is the `triggers` lists in `scripts/test-suites.mjs`; CLAUDE.md carries the full table): an `ENGINE_VERSION` bump owes `bun run test:new-work-e2e` (Playwright, offline), `bun run test:live-e2e-accept-cleanup` (provider-billed), and `bun run test:live-svelte-adapter-deepseek` (DeepSeek-billed) on top of the default run. ## Anti-pattern detection rules -`cli/engine/detect-antipatterns.mjs` is the source of truth for the rule engine. It feeds the CLI, the site overlay (`cli/engine/detect-antipatterns-browser.js`, regenerated by `bun run build:browser`), the Chrome extension (`extension/detector/`, regenerated by `bun run build:extension`), and the homepage `DETECTION_COUNT` in `site/public/js/generated/counts.js` (regenerated by `bun run build`). After any rule change run all three builds plus `bun run test` so nothing drifts. - -TDD order is non-negotiable: - -1. Add a fixture at `tests/fixtures/antipatterns/{rule-id}.html` with two columns (should-flag / should-pass), each case identified by a unique heading. ≥4 flag cases and ≥5 false-positive shapes. **Use explicit pixel dimensions in CSS** — jsdom does no layout. -2. Add a failing test in `tests/detect-antipatterns-fixtures.test.mjs` using the snippet-substring pattern (regex `/"([^"]+)"/` against `SHOULD_FLAG` / `SHOULD_PASS` lists). -3. Add the rule entry to the `ANTIPATTERNS` array (`id`, `category` = `slop` or `quality`, `name`, `description`, optional `skillSection` / `skillGuideline`). -4. Implement a pure `checkXxx(opts)` returning `[{ id, snippet }]` — no DOM access inside. -5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `cli/engine/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake. -6. Verify on a live page at `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree. - -Conventions: wrap the identifying heading text in straight double quotes inside snippets so the fixture test can extract it. jsdom-specific helpers `resolveBackground()`, `resolveGradientStops()`, and `parseGradientColors()` exist because `background:` shorthand isn't decomposed and computed colors aren't normalized in jsdom — use them. Reference rules to copy from: `side-tab` (border), `low-contrast` (color+gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level). +The rule engine lives in the engine repo, not here. What this repo owns is the behavior contract: `docs/CLI-CONTRACT.md` describes every verb, `tests/oracle/` holds the recorded goldens and replays them against the binary (`tests/oracle.test.mjs`), and `tests/fixtures/antipatterns/*.html` are the fixtures those goldens scan. A rule change lands in the engine, then here as a new oracle case (`node tests/oracle/record.mjs --bin `, golden reviewed by hand) and, when it introduces new design guidance, an edit to `skill/SKILL.src.md` or `skill/reference/*.md`. Rule counts quoted in `README.md` and `README.npm.md` are checked by the build against `extension/detector/antipatterns.json` when that vendored file is present. ## Commit & Pull Request Guidelines @@ -88,4 +77,4 @@ Tags are per-component because the three components ship independently: `skill-v ## Contributor Notes -Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `skill/`, `scripts/`, or `cli/`, then regenerate artifacts for validation. Stage generated harness artifacts only for release/main-sync or build-system work. +Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `skill/`, `scripts/`, or `cli/` (or the engine repo for verb behavior), then regenerate artifacts for validation. Stage generated harness artifacts only for release/main-sync or build-system work. diff --git a/CLAUDE.md b/CLAUDE.md index dc3dae89a..00a629f6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,21 @@ There is **one** user-invocable skill, `impeccable`, with **23 commands** undern - `SKILL.src.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table. Provider `SKILL.md` files are generated from this source. - `reference/` — one `.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.), the shared playbooks the router loads outside the command table (`new-work.md`, `craft-floor.md`, `operate.md`, `routing.md`), and the native platform references (`ios.md`, `android.md`). When a sub-command is matched, the router loads its reference file. -- `scripts/command-metadata.json` — single source of truth for each command's description, argument hint, and (eventually) category. Both the build and `pin.mjs` read from this. -- `scripts/pin.mjs` — creates/removes lightweight redirect shims so users can have `/audit` as a standalone shortcut that delegates to `/impeccable audit`. +- `scripts/command-metadata.json` — single source of truth for each command's description, argument hint, and (eventually) category. Both the build and the engine's `pin` verb read from this. +- `scripts/impeccable` (+ `impeccable.cmd`, `VERSION`): the launcher every skill verb goes through. See **Engine binary** below. +- `impeccable pin` — an engine verb that creates/removes lightweight redirect shims so users can have `/audit` as a standalone shortcut that delegates to `/impeccable audit`. + +### Engine binary (the runtime behind every verb) + +The skill has no runtime of its own. Every command the skill text runs is `{{scripts_path}}/impeccable ` (Setup step 1 says `impeccable context`; `impeccable.cmd` is the Windows twin for shells without `sh`). `skill/scripts/impeccable` is a POSIX `sh` launcher: it execs `$IMPECCABLE_BIN` if set, else the sibling `scripts/bin/-/impeccable[.exe]`, else `~/.impeccable/bin/impeccable`, else the version-pinned user cache `~/.impeccable/bin//`, else `impeccable` on PATH, and as a last resort downloads the pinned version into that cache. It exports `IMPECCABLE_SKILL_DIR` (the skill dir, for `reference/*.md` and `command-metadata.json`) and `IMPECCABLE_SELF` (how the binary spells itself in the commands it prints). + +The binary is built from a separate repo (`~/code/impeccable-engine`; do not edit it from here). Its verbs are the old script basenames (`context`, `doctor`, `pin`, `hook`, `hook-before-edit`, `live*`, `detect`, ...) with two aliases: `signals` for context-signals and `hooks` for hook-admin. Its observable behavior is specified in `docs/CLI-CONTRACT.md` and pinned by `tests/oracle/`. + +- **`ENGINE_VERSION`** (repo root) pins the engine release. The build copies it to `skill/scripts/VERSION`, which the launcher reads to name the download and the cache dir; `cli/bin/cli.js` reads the same version from `package.json`'s `optionalDependencies`. Bumping it is a release-time decision, like the other manifest versions. +- **Binaries are never tracked.** `skill/scripts/bin/` and `**/skills/impeccable/scripts/bin/` are gitignored, so the tracked provider dirs and `plugin/` ship launcher-only and users get the binary on first run. `bun run build:release` fetches every target (`scripts/fetch-engine.mjs --all --lenient`) and stages `bin//` into the dist skill copies **after** the root harness dirs and `plugin/` were synced, so `dist/universal.zip` is self-contained while git stays clean; `IMPECCABLE_BUNDLE_ENGINE=0` keeps the zip launcher-only. +- **Tests get a binary** from `IMPECCABLE_BIN` or `skill/scripts/bin//` (`bun run fetch:engine`; `IMPECCABLE_BIN= bun run fetch:engine` copies a local build there). `tests/lib/engine-bin.mjs` is the one resolver; suites that need the binary skip cleanly without it. +- **The oracle is the behavior gate.** `tests/oracle/` holds goldens recorded from the JS scripts before they left the tree, plus reviewed deltas in `DELTAS.md`; `tests/oracle.test.mjs` replays them against the binary in `bun run test`. New cases are recorded from the binary (`record.mjs --bin`) and reviewed by hand. `tests/oracle/vectors/calls/` is the frozen function-level snapshot; it cannot be regenerated. +- **What stays JavaScript here:** the in-page live-mode JS (`skill/scripts/live-browser*.js`, `modern-screenshot.umd.js`), the build and test tooling, the extension shell, and the npm shim. **Do not add standalone skills** unless there's a strong reason. The consolidation was deliberate: the `/` menu pollution problem is real and gets worse as users install more plugins. @@ -39,36 +52,36 @@ A second axis, **orthogonal to mode**. Mode answers "what does the visitor come - **android** — a native Android app. Loads `reference/android.md` (Material Design 3 distilled). - **adaptive** — a cross-platform app shipping both iOS and Android from one codebase (Flutter, React Native, KMP) that adapts per OS. Loads **both** `reference/ios.md` and `reference/android.md`. A Flutter/RN app that uses one look on both platforms (Material-everywhere is the Flutter default) is not adaptive; it takes that single platform's value. -PRODUCT.md carries a `## Platform` section with a bare value (`web` / `ios` / `android` / `adaptive`). It's parsed by `extractPlatform()` in `skill/scripts/context.mjs`, built on the generic `extractSectionValue()` helper; a **missing field defaults to `web`** so legacy projects are unaffected. A line that names both native targets (e.g. `ios, android`) is also read as `adaptive`; any other unrecognized value falls back to web **and** the `context.mjs` CLI prints a WARNING directive naming the bad value, so a toolchain name or typo never silently gets web guidance. `context.mjs` inlines the native reference(s) directly into its output when the value is `ios`, `android`, or `adaptive` (both), so native conventions land in context without a second model-directed read. `init` (Step 3) confirms an ambiguous platform as part of the product-truth interview, and Step 4 records it as the bare value. +PRODUCT.md carries a `## Platform` section with a bare value (`web` / `ios` / `android` / `adaptive`). The `context` verb parses it; a **missing field defaults to `web`** so legacy projects are unaffected. A line that names both native targets (e.g. `ios, android`) is also read as `adaptive`; any other unrecognized value falls back to web **and** `impeccable context` prints a WARNING directive naming the bad value, so a toolchain name or typo never silently gets web guidance. `impeccable context` inlines the native reference(s) directly into its output when the value is `ios`, `android`, or `adaptive` (both), so native conventions land in context without a second model-directed read. `init` (Step 3) confirms an ambiguous platform as part of the product-truth interview, and Step 4 records it as the bare value. `ios.md` and `android.md` are distilled from the MIT-licensed [ehmo/platform-design-skills](https://github.com/ehmo/platform-design-skills); attribution is in `NOTICE.md`. Where a command's native guidance diverges too much to share a file, it gets a **native variant**: `reference/.native.md`, listed in SKILL.md's Commands table and routed **instead of** the web file when `setup.platform` is native (Setup step 2). One variant covers ios, android, and adaptive; per-OS specifics stay in the platform refs, which Setup loads regardless. Variants today: `audit.native.md`, `adapt.native.md` (their web files carry a one-line web-only guard that redirects stray native readers). `audit.native.md` mirrors `audit.md`'s report skeleton; change the skeleton in both together. Commands whose divergence the platform refs already cover (`animate`, `layout`) carry nothing extra; don't add in-file translation notes, they make native runs pay for web content. -**Live mode, the `detect` CLI, and the design hook are web-only.** They operate on a browser / HTML rules, so SKILL.md's routing skips live and `detect.mjs` for any native (`ios` / `android` / `adaptive`) project, and the hook (`hook-lib.mjs` `resolveProjectPlatform` / `isNativePlatform`, also used by `hook-before-edit.mjs`) skips its scan when PRODUCT.md declares a native platform — a React Native project is made of exactly the `.tsx` / `.ts` / `.js` files the hook watches. +**Live mode, `impeccable detect`, and the design hook are web-only.** They operate on a browser / HTML rules, so SKILL.md's routing skips live and `impeccable detect` for any native (`ios` / `android` / `adaptive`) project, and the `hook` and `hook-before-edit` verbs skip their scan when PRODUCT.md declares a native platform — a React Native project is made of exactly the `.tsx` / `.ts` / `.js` files the hook watches. ### Artifact staleness and the doctor pass Impeccable writes files into user projects, so a released version has to cope with artifacts an older one wrote. Three kinds of drift travel under "out of date" and they are handled separately: -1. **Tool version drift** (installed skill older than published). `computeUpdateDirective()` in `context.mjs`, emitted as `UPDATE_AVAILABLE`. Predates this system, unchanged. -2. **Schema drift** (an artifact carries fields nothing reads, is missing fields now expected, or sits in a retired location). Deterministic. `skill/scripts/lib/staleness.mjs`. +1. **Tool version drift** (installed skill older than published). Emitted by `impeccable context` as `UPDATE_AVAILABLE`. Predates this system, unchanged. +2. **Schema drift** (an artifact carries fields nothing reads, is missing fields now expected, or sits in a retired location). Deterministic; the engine's staleness module. 3. **Truth drift** (the code moved on and the document no longer describes it). Not mechanical. `document` and `init` own the rewrite; the deep pass measures a proxy and is required to say it is a proxy. **Two tiers, and the split is a performance contract, not a preference.** -- **Tier 1** is `collectBootFindings()` in `lib/staleness.mjs`, called from `appendStalenessDirective()` in `context.mjs`. It may only spend what a boot already spends: markdown already in memory, a bounded set of stats, and the small JSON files the boot reads regardless. **No directory walks, no git, no cross-workspace sweep.** The one walk it uses (`discoverTargetCandidates`) is one `resolveTargetSelection` has already paid for. Adding an expensive check here taxes every session in every project. -- **Tier 2** is `lib/staleness-deep.mjs`, run on demand by `skill/scripts/doctor.mjs`. Git log, per-workspace sweep, ignore-list validation against the live `ANTIPATTERNS` registry, hook script resolution. +- **Tier 1** runs inside `impeccable context` at boot. It may only spend what a boot already spends: markdown already in memory, a bounded set of stats, and the small JSON files the boot reads regardless. **No directory walks, no git, no cross-workspace sweep.** The one walk it uses is the target-candidate discovery the boot has already paid for. Adding an expensive check here taxes every session in every project. +- **Tier 2** is the deep pass behind `impeccable doctor`, run on demand. Git log, per-workspace sweep, ignore-list validation against the live rule registry, hook launcher resolution. **Findings are data.** `{ id, artifact, path, severity, summary, fix }`, so the boot directive, the text report, and `--json` all render one set. Severity says what should happen, not how bad it is: `auto` (fix silently on the next write to that file), `mention` (state once, carry on), `route` (name the command that owns the repair). `doctor --fix` applies only `auto`, and only where no judgment is involved. -**Emission discipline.** Boot output is already heavy, so Tier 1 emits **one** `CONTEXT_STALE` directive for the whole set, and `lib/staleness-notice.mjs` throttles `mention` and `route` findings to once a week per project (cached in `~/.impeccable/staleness-check.json`, alongside the update cache, so no gitignore entry is owed). `auto` findings are never throttled and never shown to the user. Opt out with `"stalenessCheck": false` or `IMPECCABLE_NO_STALENESS_CHECK=1`. **A test that asserts on other boot directives should set that env var**, which is why the update-check suite in `tests/context.test.mjs` does. +**Emission discipline.** Boot output is already heavy, so Tier 1 emits **one** `CONTEXT_STALE` directive for the whole set, and `mention` and `route` findings are throttled to once a week per project (cached in `~/.impeccable/staleness-check.json`, alongside the update cache, so no gitignore entry is owed). `auto` findings are never throttled and never shown to the user. Opt out with `"stalenessCheck": false` or `IMPECCABLE_NO_STALENESS_CHECK=1`. **An oracle case that asserts on other boot directives should pin that env var.** -**Provenance stamps.** PRODUCT.md carries `` (constants in `lib/artifact-schema.mjs`, template in `init.md`). Without it, every check is a heuristic reconstruction of what era a file came from. **Stamps are schema versions, not release versions**: a PRODUCT.md written by v4.0.0 is not stale under v4.0.1, and a schema version changes only when the shape does. **DESIGN.md deliberately carries no stamp** because it follows the external design.md spec that Stitch's linter validates, and every DESIGN.md signal (sidecar `schemaVersion`, sidecar mtime, section coverage, git drift) is measurable without one. +**Provenance stamps.** PRODUCT.md carries `` (schema constants live in the engine; template in `init.md`). Without it, every check is a heuristic reconstruction of what era a file came from. **Stamps are schema versions, not release versions**: a PRODUCT.md written by v4.0.0 is not stale under v4.0.1, and a schema version changes only when the shape does. **DESIGN.md deliberately carries no stamp** because it follows the external design.md spec that Stitch's linter validates, and every DESIGN.md signal (sidecar `schemaVersion`, sidecar mtime, section coverage, git drift) is measurable without one. -**When you retire a PRODUCT.md field, add it to `PRODUCT_DEPRECATED_SECTIONS`** in `lib/artifact-schema.mjs` with the reason. The reason is not decoration: told only that a field is deprecated, models preserve it "just in case", which is how a retired axis keeps steering current output. +**When you retire a PRODUCT.md field, add it to the engine's deprecated-sections list** with the reason (and record the new boot output as an oracle case). The reason is not decoration: told only that a field is deprecated, models preserve it "just in case", which is how a retired axis keeps steering current output. -**`doctor` is a utility command, not a design command.** It follows the `hooks` and `pin` pattern (a line in SKILL.src.md plus `reference/doctor.md`), not the Commands-table pattern. It is deliberately **not** in `IMPECCABLE_SUB_COMMANDS`, `command-metadata.json`, `SKILL_CATEGORIES`, or `pin.mjs`'s `VALID_COMMANDS`, and it does not count toward the 23. Keep maintenance tooling out of the design menu. +**`doctor` is a utility command, not a design command.** It follows the `hooks` and `pin` pattern (a line in SKILL.src.md plus `reference/doctor.md`), not the Commands-table pattern. It is deliberately **not** in `IMPECCABLE_SUB_COMMANDS`, `command-metadata.json`, `SKILL_CATEGORIES`, or the `pin` verb's valid-command list, and it does not count toward the 23. Keep maintenance tooling out of the design menu. ## Repo split: public product vs private service (impeccable-site) @@ -76,7 +89,7 @@ As of v4 the repo holds only the open-source product layer: the skill, CLI, exte Consequences here: -- `skill/scripts/concept-seed.mjs` has no local catalog. It resolves data via `IMPECCABLE_CATALOG_DIR` (private repo, evals, tests), then the roll API at impeccable.style, then a degraded promotion-only seed. Tests run against `tests/fixtures/concept-catalog/`. +- `impeccable concept-seed` has no local catalog. It resolves data via `IMPECCABLE_CATALOG_DIR` (private repo, evals, tests), then the roll API at impeccable.style, then a degraded promotion-only seed. Oracle cases run against `tests/fixtures/concept-catalog/`. - The choice-ping telemetry (`--chosen`) honors `DO_NOT_TRACK` and `IMPECCABLE_NO_TELEMETRY` and only fires for API-dealt rolls. - Site copy, changelog, theme, and count validation for site pages happen in impeccable-site; this repo's `validateProse` scans only the READMEs. - The release script reads the changelog from `../impeccable-site/site/pages/changelog.astro` when releasing from here. @@ -90,7 +103,7 @@ The build's `validateProse` step (in `scripts/build.js`) enforces a denylist: em `validateProse` scans `README.md` and `README.npm.md`; site copy is validated in impeccable-site. -**`skill/` is checked too, by a second gate.** `validateProse` skips it because the full ruleset does not fit LLM-facing reference instructions. `validateSkillProse` then scans `skill/**/*.md` (markdown only, not `skill/scripts/**` code or comments) and fails the build on em dashes plus the subset of phrases with no technical reading: `load-bearing`, `highest-leverage`, `biggest unlock`, `reflex defaults`, `collapses into monoculture`, `data-driven`, `delve`, `tapestry`, `in today's`, `gone are the days`, `let's dive in`, `in summary`, `in conclusion`. The words it does *not* enforce in `skill/` (`seamless`, `robust`, `elevate`, and friends) are the ones with legitimate technical uses. Net effect: an em dash in `skill/reference/*.md` fails `bun run build`; an em dash in a `skill/scripts/*.mjs` code comment does not. +**`skill/` is checked too, by a second gate.** `validateProse` skips it because the full ruleset does not fit LLM-facing reference instructions. `validateSkillProse` then scans `skill/**/*.md` (markdown only, not the launcher or page JS under `skill/scripts/`) and fails the build on em dashes plus the subset of phrases with no technical reading: `load-bearing`, `highest-leverage`, `biggest unlock`, `reflex defaults`, `collapses into monoculture`, `data-driven`, `delve`, `tapestry`, `in today's`, `gone are the days`, `let's dive in`, `in summary`, `in conclusion`. The words it does *not* enforce in `skill/` (`seamless`, `robust`, `elevate`, and friends) are the ones with legitimate technical uses. Net effect: an em dash in `skill/reference/*.md` fails `bun run build`; an em dash in a `scripts/*.js` code comment does not. The deeper structural issues (negation pivot, triadic auto-pilot, uniform paragraph rhythm, hollow confidence) require human judgment. `docs/STYLE.md` lists them. Use them on every editorial pass. @@ -100,11 +113,14 @@ The build system compiles the impeccable skill from `skill/` to provider-specifi ```bash bun run build # Build dist/ provider output without syncing root harness dirs -bun run build:release # Build dist/ provider output and sync root harness dirs + plugin/ +bun run build:release # Build dist/ provider output, sync root harness dirs + plugin/, stage engine binaries into dist zips bun run rebuild # Clean and rebuild without root harness sync bun run rebuild:release # Clean and rebuild with root harness sync +bun run fetch:engine # Download the pinned engine binary for this machine into skill/scripts/bin/ ``` +The skill's `scripts/` payload is copied verbatim to every provider (launcher with its executable bit, `impeccable.cmd`, `VERSION`, `command-metadata.json`, page JS); nothing under `skill/scripts/bin/` is read as source. `build:browser` is a stub: the in-page detector bundle is produced by the engine repo and vendored under `extension/detector/` by `build:extension`. + Source files use placeholders that get replaced per-provider: - `{{model}}` — Model name (Claude, Gemini, GPT, etc.) - `{{config_file}}` — Config file name (CLAUDE.md, .cursorrules, etc.) @@ -142,7 +158,7 @@ bun run test:skill-behavior # Opt-in: LLM-backed checks that the skill text ac bun run test:plugin-e2e # Just the plugin loader E2E (also part of the default suite) ``` -Unit tests (build orchestration, detector logic) run via `bun test`. Fixture tests (jsdom-based HTML detection) run via `node --test` because bun is too slow with jsdom. The `test` script handles this split automatically. +Unit tests (build orchestration, transformers, validators) run via `bun test`. Everything that spawns the engine binary (`tests/oracle.test.mjs`, `tests/framework-fixtures.test.mjs`) runs via `node --test`; both skip cleanly when no binary is found (`bun run fetch:engine` or `IMPECCABLE_BIN`). The `test` script handles this split automatically. Verb behavior is not unit-tested here at all: the oracle goldens and the engine repo's own tests own it. ### Which opt-in suite a change owes @@ -150,14 +166,15 @@ The default suite does not cover everything. When a change touches one of these | Area touched | Run | Cost | |---|---|---| -| `skill/scripts/live-*.{mjs,js}`, `skill/scripts/live/**` | `bun run test:live-e2e` | ~2 min, real npm installs + dev servers, needs Playwright Chromium | -| `live-accept` / `live-browser` / `live-server` / `live-wrap` / `live/sveltekit-adapter` | also `bun run test:live-e2e-accept-cleanup` | bills a provider API key | -| `live/sveltekit-adapter.mjs`, `live/svelte-component.mjs` | `bun run test:live-svelte-adapter-deepseek` | bills DeepSeek | -| `SKILL.src.md` Setup, `context.mjs`, Setup-adjacent reference files | `bun run test:skill-behavior` | ~5 min, bills all four provider keys | -| `serve-question.mjs`, `generate-image.mjs`, `concept-seed.mjs` | `bun run test:new-work-e2e` | Playwright, offline, no API cost | -| `cli/bin/commands/skills.mjs` | `bun run test:cli-remote-e2e` | hits impeccable.style | +| `ENGINE_VERSION` bump, `skill/scripts/live-browser*.js` | `bun run test:live-e2e` | ~2 min, real npm installs + dev servers, needs Playwright Chromium | +| `ENGINE_VERSION` bump | also `bun run test:live-e2e-accept-cleanup` | bills a provider API key | +| `ENGINE_VERSION` bump | `bun run test:live-svelte-adapter-deepseek` | bills DeepSeek | +| `SKILL.src.md` Setup, Setup-adjacent reference files, `ENGINE_VERSION` bump | `bun run test:skill-behavior` | ~5 min, bills all four provider keys | +| `ENGINE_VERSION` bump | `bun run test:new-work-e2e` | Playwright, offline, no API cost | | `plugin/`, `skill/agents/`, `scripts/build.js`, plugin manifest validator | `bun run test:plugin-e2e` | ~1 s; already in the default suite, needs the `claude` CLI | +Verb-level behavior changes happen in the engine repo; the check they owe here is `bun run test` with a binary present (the oracle), and a new oracle case when the contract grows. + **Plugin loader E2E** (`tests/plugin-e2e.test.mjs`, in the default suite): installs the committed `./plugin` subtree into a real Claude Code, sandboxed via `CLAUDE_CONFIG_DIR` in a temp dir, and asserts the component inventory from `claude plugin details`: the skill parses, every `plugin/agents/*.md` is visible, hooks are discovered. This is the only check that catches loader-contract surprises the unit guards can't know about (PR #494 shipped an `agents` manifest key that silently loaded zero agents; `claude plugin validate` never flags plugin-manifest problems). Runs in about a second; skips cleanly when the `claude` CLI is not on PATH. The known contract itself (allowed manifest keys, no `agents` key, trailing-slash `skills` path, source agents shipped) is pinned deterministically by `scripts/lib/validate-plugin-manifest.js`, unit-tested in `tests/validate-plugin-manifest.test.js` and enforced as a `bun run build` gate. Never add a key to the generated plugin manifest without verifying it against a real install and extending `KNOWN_LOADER_KEYS`. **Important:** `tests/build.test.js` uses `spyOn(transformers, 'transformCursor')` with the named exports from `scripts/lib/transformers/index.js`. Those named exports (`transformCursor`, `transformClaudeCode`, etc.) are kept specifically for test spying, even though `build.js` itself uses `createTransformer + PROVIDERS` directly. **Do not delete them as "dead code"** — I made that mistake once and broke 8 tests. @@ -174,13 +191,13 @@ IMPECCABLE_E2E_DEBUG=1 bun run test:live-e2e # dump page DOM + de **One-time setup**: `npx playwright install chromium` (the suite uses a specific Chromium build keyed to the bundled Playwright version). -**Kept out of the default `bun run test`** because (a) it does real `npm install` per fixture, (b) it boots framework dev servers, (c) wall time is ~2 minutes, and (d) it requires Playwright's browser cache. Run it locally before shipping changes to anything in `skill/scripts/live-*.{mjs,js}` or `skill/scripts/live/**`. +**Kept out of the default `bun run test`** because (a) it does real `npm install` per fixture, (b) it boots framework dev servers, (c) wall time is ~2 minutes, and (d) it requires Playwright's browser cache. Run it locally before shipping changes to the page JS or before bumping `ENGINE_VERSION`. (Its helpers still drive the live verbs by script path; retargeting them at the launcher is pending.) -Three live-mode invariants worth knowing before editing (established by the 2026-07 rewrite, full rationale in `docs/LIVE-REWRITE-PLAN.md`): +Three live-mode invariants worth knowing before editing (established by the 2026-07 rewrite, full rationale in `docs/LIVE-REWRITE-PLAN.md`; the implementation is the engine's `live` crate now, the contract is unchanged): -- **Roots.** `skill/scripts/live/roots.mjs` resolves appRoot/repoRoot/contextRoot once at boot and persists `.impeccable/live/roots.json`; every live CLI calls `enterLiveRoot()` in its main guard and chdirs onto the manifest's appRoot. Never derive a live path from ambient cwd in a new script; go through the manifest. +- **Roots.** `impeccable live` resolves appRoot/repoRoot/contextRoot once at boot and persists `.impeccable/live/roots.json`; every live verb re-anchors on that manifest and chdirs onto its appRoot. Never derive a live path from ambient cwd; go through the manifest. - **Svelte preview modules must live under `node_modules/.impeccable-live`.** SvelteKit restricts vite `server.fs.allow` to src/lib, src/routes, .svelte-kit, and node_modules; a preview tree under `.impeccable/` 403s. Staleness is handled by per-publish revision dirs (`r/`, bumped by the server on every done-reply), not by file watching. -- **`svelte` is a devDependency for tests only.** The AST scaffolder (`live/svelte-ast.mjs`) and accept pipeline (`live/accept-css.mjs`) resolve the compiler from the USER app's node_modules at runtime; unit tests and the static fixture sweep symlink this repo's copy into staged fixtures. Skill scripts still ship dependency-free. +- **`svelte` is a devDependency for tests only.** The Svelte scaffolder and accept pipeline resolve the compiler from the USER app's node_modules at runtime; the fixture sweep and oracle cases symlink this repo's copy into staged fixtures. The agent is pluggable via a one-method interface in `tests/live-e2e/agent.mjs`: `generateVariants(event, context) → { scopedCss, variants[] }`. The default fake agent emits canned variants that exercise all three param kinds (`range`, `steps`, `toggle`). The orchestrator (wrap, write, accept, carbonize) is agent-agnostic. @@ -208,37 +225,33 @@ IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1 bun run test:skill-behavior # dump per-sc **Adding a scenario.** Write the fixture in `tests/skill-behavior/fixtures.mjs`, add the `it()` block in `scenarios.test.mjs` (the harness uses the source `skill/` dir via a symlink, so no rebuild needed), and update the baseline table in the suite's README. The harness's `fileLoaded(trace, filename)` helper checks both `read` and bash `cat` — different models prefer different tools. -**The harness symlinks source, not built output.** This is deliberate so SKILL.md / reference / `scripts/context.mjs` edits show up immediately without `bun run build:skills`. The trade-off: reference files surface their raw `{{placeholders}}`, but the assertions key on tool calls rather than content, so it doesn't matter for correctness. +**The harness symlinks source, not built output.** This is deliberate so SKILL.md / reference edits show up immediately without `bun run build:skills`; the launcher under `skill/scripts/` resolves the binary the same way tests do. The trade-off: reference files surface their raw `{{placeholders}}`, but the assertions key on tool calls rather than content, so it doesn't matter for correctness. ## CLI -The CLI lives in this repo under `cli/`: `cli/bin/` (entry + sub-commands), `cli/engine/` (the detect-antipatterns rule engine + browser variant), `cli/lib/` (helpers shared by CLI and Cloudflare Pages Functions). Published to npm as `impeccable`. +`cli/` is the npm package `impeccable`, now a thin shim: `cli/bin/cli.js` locates the engine binary (`IMPECCABLE_BIN`, then the `@impeccable/cli--` optional dependency pinned at `ENGINE_VERSION`, then `~/.impeccable/bin//`, then a checksum-verified download into that cache) and execs it with argv. The verbs users see (`detect`, `ignores`, `install`, `update`, `check`, `link`, `help`, the legacy `skills` namespace) are the binary's. `cli/platform-packages/-/package.json` are the templates the engine release publishes; the version pinned in `package.json` `optionalDependencies` must equal `ENGINE_VERSION`. ```bash npx impeccable detect [file-or-dir-or-url...] # detect anti-patterns -npx impeccable detect --fast --json src/ # regex-only, JSON output -npx impeccable live # start browser overlay server -npx impeccable skills install # install skills -npx impeccable --help # show help +npx impeccable detect --json src/ # JSON output +npx impeccable install # install skills +npx impeccable --help # show help ``` -The browser detector (`cli/engine/detect-antipatterns-browser.js`) is generated from the main engine. After changing `cli/engine/detect-antipatterns.mjs`, rebuild it: - -```bash -bun run build:browser -``` - -**IMPORTANT**: Always use `node` (not `bun`) to run the detect CLI. Bun's jsdom implementation is extremely slow and will cause scans with HTML files to hang for minutes. +The package no longer exports a JS detector API (`main` / `exports` are gone); the in-page bundle for the extension and site comes from the engine repo. ## Versioning **Feature PRs do not bump versions and do not add changelog entries.** Bumping is a release step, not part of the change that earns the release: a version in a feature branch conflicts with every other open branch, and a changelog entry describes a release that has not happened. Land the code first; the maintainer bumps and writes the changelog when cutting the release. This holds even though the "Bump when: ..." notes below name the source dirs — those say *which* component a change belongs to, not *when* to edit the manifest. The only PR that touches a manifest version is one whose purpose is the release itself. -There are three independently versioned components. Only bump the one(s) that actually changed: +There are three independently versioned components plus the engine pin. Only bump the one(s) that actually changed: + +**Engine pin** (`ENGINE_VERSION`, root): +- The engine release the launcher downloads and the npm shim's `optionalDependencies` pin. Bump it when a new engine release is published; keep `package.json` `optionalDependencies` at the same version and run `bun run build` (it rewrites `skill/scripts/VERSION`). A skill release that needs the new engine bumps this together with the skill version. **CLI** (npm package): - `package.json` → `version` -- Bump when: CLI code changes (`cli/bin/`, `cli/engine/detect-antipatterns.mjs`, etc.) +- Bump when: CLI shim code changes (`cli/bin/cli.js`, `cli/platform-packages/`) **Skills** (Claude Code plugin / skill definitions): - `.claude-plugin/plugin.json` → `version` (source of truth) @@ -283,7 +296,7 @@ All commands live under `/impeccable`. To add a new one: 2. Add a row to the **Sub-command reference table** in `skill/SKILL.src.md` 3. Add an entry to the **Command menu** section in the same file 4. Add the command name to `IMPECCABLE_SUB_COMMANDS` in `scripts/lib/utils.js` -5. Add it to `VALID_COMMANDS` in `skill/scripts/pin.mjs` +5. Add it to the `pin` verb's valid-command list in the engine repo (and record the pin/unpin oracle case) 6. Add its metadata (description + argumentHint) to `skill/scripts/command-metadata.json` 7. Add its category to `SKILL_CATEGORIES` in `scripts/lib/skill-categories.js` 8. Add its relationships to `COMMAND_RELATIONSHIPS` in impeccable-site's `sub-pages-data.js` @@ -301,39 +314,17 @@ The build validator (`generateCounts` in `scripts/build.js`) checks these files ## Adding or modifying anti-pattern detection rules -`cli/engine/detect-antipatterns.mjs` is the source of truth for the rule engine. It powers the CLI, the public-site overlay, the Chrome extension, and the homepage rule count. Five places stay in sync: +The rule engine lives in the engine repo (`crates/core` is the pure rule core, `crates/html` the static HTML engine, `crates/browser` the URL engine, plus a WASM build for the extension, the live overlay, and the site). Nothing here implements a rule. What this repo owns and keeps in sync: | Where | How it stays in sync | |---|---| -| `cli/engine/detect-antipatterns.mjs` (`ANTIPATTERNS` array + `checkXxx` logic) | Hand-edited | -| `cli/engine/detect-antipatterns-browser.js` | `bun run build:browser` | -| `extension/detector/detect.js` + `extension/detector/antipatterns.json` | `bun run build:extension` | -| impeccable-site `site/public/js/generated/counts.js` | its own build | +| `docs/CLI-CONTRACT.md` | Hand-edited: the observable contract of `impeccable detect` and every other verb | +| `tests/fixtures/antipatterns/{rule-id}.html` | Hand-edited fixture (two columns, should-flag / should-pass, unique headings, explicit pixel dimensions) | +| `tests/oracle/golden/*` | Recorded from the binary with `node tests/oracle/record.mjs --bin detect-`, reviewed by hand | +| `extension/detector/` | Vendored from the engine by `bun run build:extension`; the build's rule-count check reads `antipatterns.json` when present | | `skill/SKILL.src.md` and `reference/*.md` | Hand-edited if the rule introduces new design guidance | -Always run all three builds and the test suite after a rule change: - -```bash -bun run build && bun run build:browser && bun run build:extension && bun run test -``` - -### TDD order (non-negotiable) - -1. **Fixture** at `tests/fixtures/antipatterns/{rule-id}.html` with two columns (should-flag / should-pass), each case identified by a unique heading. Cover ≥4 flag cases and ≥5 false-positive shapes. Use **explicit pixel dimensions in CSS** because jsdom does no layout. -2. **Failing test** in `tests/detect-antipatterns-fixtures.test.mjs` using the snippet-substring pattern (regex `/"([^"]+)"/` against `SHOULD_FLAG` / `SHOULD_PASS` lists). Run it and watch it fail before implementing. -3. **Rule entry** in the `ANTIPATTERNS` array: `id`, `category` (`slop` for AI tells, `quality` for real design or a11y issues), `name`, `description`, optional `skillSection` and `skillGuideline`. -4. **Pure check function** `checkXxx(opts)` returning `[{ id, snippet }]`. No DOM access in the pure function. -5. **Two adapters**: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). `cli/engine/detect-antipatterns.mjs` is now a thin facade over `cli/engine/{registry,rules,engines,shared}`: the registry entry goes in `registry/antipatterns.mjs`, the pure check + adapters in `rules/checks.mjs`, and the wiring into **both** element loops in `engines/static-html/detect-html.mjs` (jsdom) and `browser/injected/index.mjs` (concatenated into the browser bundle). Forgetting one loop is the most common mistake; symptom is "test passes, live page silent" or vice versa. -6. **Verify on a live page**: `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and the homepage (no false positives). The two adapter paths can disagree, so manual browser checks catch what the fixture test can't. - -### Conventions and jsdom gotchas - -- **Snippet format**: wrap the identifying heading text in straight double quotes (e.g. `'icon tile above h3 "Lightning Fast"'`) so the fixture test can extract it. For rules not anchored to a heading, pick another stable identifier. -- **jsdom doesn't lay out**: `getBoundingClientRect()` returns 0×0. Read `parseFloat(style.width)` and `parseFloat(style.height)` from explicit CSS instead. -- **`background:` shorthand isn't decomposed in jsdom**: use the existing `resolveBackground()` and `resolveGradientStops()` helpers (in `engines/static-html/detect-html.mjs`). -- **Computed colors aren't normalized in jsdom**: `parseGradientColors()` handles both hex and rgb forms. - -Reference rules to copy from (all in `cli/engine/rules/checks.mjs`): `side-tab` (border), `low-contrast` (color + gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level), `kicker-above-heading` (heading-anchored with rule-ownership stand-down). +Order for a new rule: fixture here first, rule in the engine against that fixture, oracle case + golden here, then `bun run build && bun run test` with a binary present. Rule counts quoted in `README.md` / `README.npm.md` are validated by `generateCounts` against the vendored registry. ## Evals Framework (separate private repo) diff --git a/README.md b/README.md index b32ce194a..b0f48dafe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Impeccable -Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 61 deterministic detector rules for AI-generated frontend design. +Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 59 deterministic detector rules for AI-generated frontend design. > **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style). @@ -13,7 +13,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th Impeccable adds: - **One setup flow.** `/impeccable init` writes `PRODUCT.md` and offers `DESIGN.md`, so later commands know the audience, brand/product lane, voice, anti-references, colors, type, and components. - **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more. -- **61 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. +- **59 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. ## What's Included @@ -95,6 +95,8 @@ Visit [the Neo Mirai case study](https://impeccable.style/cases/neo-mirai) to se ## Installation +The skill needs no runtime of its own. Every skill copy ships a small launcher (`scripts/impeccable`, plus `impeccable.cmd` for Windows) that runs the Impeccable engine, a self-contained binary that either sits next to the launcher or is downloaded once on first run into `~/.impeccable/bin/`. Node is only involved if you use the `npx impeccable` installer, which is a shim around the same binary; the manual and Git options below work without it. + ### Option 1: CLI installer (Recommended) From the root of your project, run: @@ -375,11 +377,13 @@ On Claude Code, GitHub Copilot, Codex, Cursor, and Grok Build, `npx impeccable i Installed hook surfaces: -- Claude Code: `.claude/settings.local.json` (gitignored, machine-local) runs `${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs`. A hook moved into the shared `settings.json` is honored in place. -- GitHub Copilot: `.github/hooks/impeccable.json` (committed, shared by the Copilot CLI and the cloud agent) runs `.github/skills/impeccable/scripts/hook.mjs`. The Copilot CLI activates it once the file is on the repository's default branch and the folder is trusted. -- Cursor: `.cursor/hooks.json` runs `.cursor/skills/impeccable/scripts/hook-before-edit.mjs`. -- Codex: `.codex/hooks.json` runs `.agents/skills/impeccable/scripts/hook.mjs`. -- Grok Build: `.grok/hooks/impeccable.json` runs `.grok/skills/impeccable/scripts/hook.mjs`. Requires `/hooks-trust` or `--trust`. Findings reach the model on Stop, not after each edit. +- Claude Code: `.claude/settings.local.json` (gitignored, machine-local) runs `${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/impeccable hook`. A hook moved into the shared `settings.json` is honored in place. +- GitHub Copilot: `.github/hooks/impeccable.json` (committed, shared by the Copilot CLI and the cloud agent) runs `.github/skills/impeccable/scripts/impeccable hook`. The Copilot CLI activates it once the file is on the repository's default branch and the folder is trusted. +- Cursor: `.cursor/hooks.json` runs `.cursor/skills/impeccable/scripts/impeccable hook-before-edit`. +- Codex: `.codex/hooks.json` runs `.agents/skills/impeccable/scripts/impeccable hook`, with a `commandWindows` sibling that calls `impeccable.cmd` for cmd.exe. +- Grok Build: `.grok/hooks/impeccable.json` runs `.grok/skills/impeccable/scripts/impeccable hook`. Requires `/hooks-trust` or `--trust`. Findings reach the model on Stop, not after each edit. + +Every command goes through the launcher shipped in the skill's `scripts/` directory (`impeccable`, or `impeccable.cmd` on Windows), guarded so a missing launcher is a silent no-op. The launcher runs the engine binary that ships next to it, or downloads the pinned version once into `~/.impeccable/bin/`. No Node or other runtime is required for the hook or the skill. The installer preserves unrelated hook entries and settings. If a hook manifest is malformed, install/update aborts by default; rerun with `--force` to back up the malformed file as `.bak` and replace it. @@ -412,12 +416,12 @@ npx impeccable update ## CLI -Impeccable includes a standalone CLI for detecting anti-patterns without an AI harness: +Impeccable includes a standalone CLI for detecting anti-patterns without an AI harness. `npx impeccable` is a small shim that runs the same engine binary the skill uses (installed as a platform-specific optional dependency, or fetched once into `~/.impeccable/bin/`); Node is needed only for `npx` itself, and you can also download the binary directly and put it on your PATH. ```bash npx impeccable detect src/ # scan a directory npx impeccable detect index.html # scan an HTML file -npx impeccable detect https://example.com # scan a URL (Puppeteer) +npx impeccable detect https://example.com # scan a URL (uses an installed Chrome, Chromium, or Edge) npx impeccable detect --json . # CI-friendly JSON output npx impeccable detect --no-config src/ # raw scan, ignoring project config/context npx impeccable ignores list # show detector ignores @@ -425,7 +429,7 @@ npx impeccable ignores add-file "src/legacy/**" npx impeccable ignores add-value overused-font Inter --reason "Brand font" ``` -The detector catches 61 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more). +The detector catches 59 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more). By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution. diff --git a/docs/CLI-CONTRACT.md b/docs/CLI-CONTRACT.md index d9f36c65a..387f955a9 100644 --- a/docs/CLI-CONTRACT.md +++ b/docs/CLI-CONTRACT.md @@ -5,9 +5,13 @@ 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. Today each verb is a script under -`skill/scripts/` or a `cli/bin` subcommand; the mapping is `JS_VERBS` in -`tests/oracle/lib.mjs`. +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.