Files
pbakaus__impeccable/package.json
T

97 lines
3.7 KiB
JSON
Raw Normal View History

2025-11-16 14:54:35 -08:00
{
"name": "impeccable",
"version": "4.1.0",
"author": "Paul Bakaus",
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
2025-11-16 14:54:35 -08:00
"keywords": [
"design",
"frontend",
"ux",
"skills",
"ai",
"anti-patterns",
"lint",
"accessibility",
"css",
"html",
"detection",
"ci-cd"
2025-11-16 14:54:35 -08:00
],
"license": "Apache-2.0",
"homepage": "https://impeccable.style",
"repository": {
"type": "git",
"url": "git+https://github.com/pbakaus/impeccable.git"
},
"engines": {
"node": ">=22.18.0"
},
"type": "module",
"bin": {
Refactor: cleaner top-level directory structure (#138) * refactor(content): merge content/site/ into site/content/ Phase 1 step 1 of the directory restructure. The dual content tree was called out in CLAUDE.md as cleanup; both trees were already in sync except for anti-patterns-catalog.js, which moves to site/data/. - Delete content/site/skills/ and content/site/tutorials/ (duplicates of site/content/, which is what Astro's content collection actually reads). - Move content/site/anti-patterns-catalog.js -> site/data/. - Update scripts/lib/sub-pages-data.js and scripts/build.js to read from site/content/ and site/data/. - Drop content/site/ from validateProse target list (site/content was already there). - Rewrite the "Two content trees" section in CLAUDE.md as a single-tree pointer; update stale dev-server text mentioning the deleted server/index.js. Tests: 186/186 pass. Skills build: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(skill): rename source/skills/impeccable/ -> skill/ Phase 1 step 2 of the directory restructure. The path was redundantly nested ("source/" wrapper plus "skills/impeccable/" — singular content hidden behind the plural). Collapses to flat skill/SKILL.md + skill/reference/ + skill/scripts/. - Move source/skills/impeccable/ -> skill/. - Rewrite scripts/lib/utils.js readSourceFiles(): drop the multi-skill iteration (CLAUDE.md commits to a single user-invocable skill); read skill/SKILL.md directly. - Update scripts/build.js, scripts/generate-og-image.js, and the sub-pages data layer to point at skill/. - Update tests/lib/utils.test.js: drop the "multi-skill" and "dir-name fallback" cases, update single-skill paths to skill/. - Update tests/build.test.js similarly: drop "multiple skills" integration test, update paths. - Update non-glob path joins in tests/framework-fixtures.test.mjs, tests/live-e2e/session.mjs, tests/live-e2e/agents/llm-agent.mjs, tools/live-loop.mjs. - Update prose/text references in CLAUDE.md, AGENTS.md, DEVELOP.md, README.md, scripts/lib/sub-pages-data.js, bin/commands/skills.mjs, site/data/anti-patterns-catalog.js, site/pages/docs/[...slug].astro, docs/adr-live-variant-mode.md, docs/plans/. Eval framework note: the separate impeccable-evals repo reads ../impeccable/source/skills/impeccable/ and needs a coordinated rename to ../impeccable/skill/. Tests: 186/186 pass. Skills build: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: rename docs/ -> notes/ Phase 1 step 3 of the directory restructure. The internal docs/ dir (ADRs and plans) clashed with the site's /docs route. Renaming it "notes/" makes the difference unambiguous: notes/ is project-internal process, /docs is the user-facing route under site/pages/docs/. No code references the dir; the rename is a clean git mv. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(site): move public/ under site/public/ Phase 2 step 4 of the directory restructure. Public assets and the Astro publicDir now live alongside the rest of the site, so site/ is fully self-contained for static content. - git mv public site/public. - astro.config.mjs: add publicDir: './site/public'. Astro defaults to ./public at the project root, so the override is required. - scripts/build.js: write generated _data, _headers, _redirects, _routes.json, and js/detect-antipatterns-browser.js into site/public/. Also delete the dead _REMOVED() Bun static-site builder (replaced by Astro at #130; the placeholder no longer earns its keep). - scripts/build.js validateProse: replace the stale public/index.html reference (deleted at the Astro migration) with site/pages/index.astro in the count-validation file list, restoring homepage drift detection. - scripts/generate-og-image.js: write OG image into site/public/. - scripts/screenshot-antipatterns.js: read examples from + write screenshots to site/public/antipattern-{examples,images}/. - scripts/lib/sub-pages-data.js: load command demos from site/public/js/demos/commands. - .gitignore: rename the public/* generator-output entries to site/public/*. - CLAUDE.md: refresh CSS/data-file paths (still pointing at the old pre-Astro public/css/ + public/js/ tree), point the changelog and command-add checklists at site/pages/index.astro and site/scripts/data.js + site/scripts/components/framework-viz.js. Cloudflare Pages note: functions/ stays at the repo root because CF Pages auto-discovers it there with no configuration knob to relocate. Moving it under site/ would either break deployment or require a build-time copy step that adds more complexity than the cleanup is worth. Tests: 186/186 pass. Skills + site build clean. _headers, _redirects, _routes.json, _data/ all land in build/ correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): consolidate bin/ + src/ + lib/ under cli/ Phase 2 step 5 of the directory restructure. The CLI surface was split across three top-level dirs whose names were easy to mistake for each other (especially src/ vs source/ pre-step-2). Consolidates under cli/. - git mv bin -> cli/bin (CLI entry + skills sub-command) - git mv src -> cli/engine (detect-antipatterns engine + browser variant) - git mv lib -> cli/lib (download-providers helper) Update package.json: - bin.impeccable: cli/bin/cli.js - main + exports: cli/engine/detect-antipatterns.mjs and the ./browser variant - files: ["cli/", "LICENSE"] Update internal references: - cli/bin/cli.js: dynamic import points at ../engine/, package.json read goes one level deeper (../../package.json). - functions/api/download/[type]/[provider]/[id].js + bundle/[provider].js: cli/lib/download-providers.js path. - scripts/build.js, scripts/build-browser-detector.js, scripts/build-extension.js: cli/engine path constants. - scripts/lib/sub-pages-data.js, scripts/lib/utils.js, skill/scripts/ live-server.mjs: comment refs. - tests/detect-antipatterns{,-browser,-fixtures}.test.{js,mjs}, tests/windows-path-fix.test.js: import + read paths. - AGENTS.md, CLAUDE.md: doc paths. Verified: - npx node cli/bin/cli.js --version, --help, detect --help all work. - bun run build, bun run build:browser, bun run build:extension all clean. Browser detector lands at cli/engine/detect-antipatterns-browser.js; extension/detector/detect.js still emits to the same location. - bun run test: 186/186 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update browser-detector paths missed in cli/ rename Bugbot caught two runtime path leaks where the comment got renamed to cli/engine/ but the actual code still used the old src/ segment. - skill/scripts/live-server.mjs: detectPaths array now joins cli, engine, detect-antipatterns-browser.js for both the repo-relative lookup (4 dirs up from .claude/skills/impeccable/scripts/ to repo root) and the npm node_modules fallback. Without this fix, the detection overlay would silently not load during live-server sessions. - scripts/build.js: the post-build copy of the browser detector into site/public/js/ was reading from src/. The if (fs.existsSync(...)) guard meant the copy was silently skipping, so antipattern-examples pages would 404 on /js/detect-antipatterns-browser.js once the site was deployed. Tests: 186/186 pass. Build clean. site/public/js/detect-antipatterns-browser.js re-emits as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cleanup-deprecated import path missed an extra .. in cli/ rename Bugbot caught three call sites in cli/bin/commands/skills.mjs that import '../../skill/scripts/cleanup-deprecated.mjs'. Pre-rename, that was correct from bin/commands/ (one parent to bin/, one to repo root). After moving the file from bin/commands/ to cli/bin/commands/, the path is one directory deeper, so it needs three .. segments to reach the repo root. Without the fix, every cleanup invocation throws on import and gets swallowed by the surrounding try/catch — silent skip. cli/bin/cli.js's package.json read already uses '../../package.json' (the same depth pattern), confirming three levels is correct. Verified: dynamic import resolves and exports the expected functions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: sweep stale path/file references missed in the restructure Same root cause as the two bugbot finds: some references in moved or related files weren't tracked because they didn't match a simple sed pattern. Caught the rest by walking each moved dir's depth and each Astro-migration deletion. Stale path references (post-Astro migration, missed earlier): - CLAUDE.md: legacy URL redirects "live in server/index.js" -> point at the actual sources (scripts/build.js generateCFConfig + site/public/_redirects). - AGENTS.md: counts.js path (public/ -> site/public/), changelog file (public/index.html -> site/pages/index.astro), screenshots note (public/ -> site/), source-of-truth dirs (source/, src/ -> skill/, cli/). - tests/detect-antipatterns-browser.test.mjs: comment about routes "in server/index.js". - skill/reference/live.md: workflow.css example for "this repo" was pre-Astro (public/css/) -> site/styles/. (User-project Vite/Next example unchanged.) Stale path that pointed at moved files: - tests/skills-cli.test.js: CLI path was '..', 'bin', 'cli.js'; now '..', 'cli', 'bin', 'cli.js'. Test isn't wired into bun run test but it would have failed if invoked. Dead files (orphaned by Astro migration, never cleaned up): - tests/server/download-validation.test.js: imported from ../../server/lib/{validation,api-handlers}.js which were deleted in b8f09c8. Test was a silent failure waiting to happen. - scripts/lib/render-markdown.js: 156-line module with zero consumers (the only caller, scripts/lib/render-page.js, was deleted in the Astro cleanup). - scripts/build.js: dead commented-out generateSubPages import. Tests: 186/186 pass. Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build): remove invalid Corepack packageManager spec Cloudflare Pages rejects the build with `Unsupported package manager specification (bun@1.3.11)`. The packageManager field follows Corepack's syntax which only validates npm/pnpm/yarn — `bun@X.Y.Z` parses as a malformed Corepack directive even though Bun itself treats it as a hint. Pre-existing on main since d874af0 (CF Pages deploy on main also failing); just surfaces here because the PR triggers a fresh deploy. CF Pages auto-detects Bun anyway (the build log confirms: "Detected the following tools from environment: bun@1.3.11, pnpm@10.11.1, nodejs@22.16.0"). Removing the field unblocks the deploy without changing local dev behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:38:03 -07:00
"impeccable": "cli/bin/cli.js"
},
"files": [
The Rust engine: one binary replaces every script and the JS detector, fully open (#714) * Add oracle harness: verb goldens and function-level vectors Records stdout/stderr/exit/files for every impeccable verb over a fixed corpus and replays them against an alternate implementation. Adds a loader hook that captures per-function call vectors from the pure engine modules. Prepared with AI assistance (Claude Code). * Oracle: hook, hook-before-edit, hook-admin cases and goldens Prepared with AI assistance (Claude Code). * Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb Prepared with AI assistance (Claude Code). * Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens Prepared with AI assistance (Claude Code). * Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon) Prepared with AI assistance (Claude Code). * Oracle: mask the binary path before HOME; export launcher env to the binary Prepared with AI assistance (Claude Code). * detect: set process.exitCode instead of exiting after the final write process.exit() right after a large piped stdout write truncated JSON output at the pipe buffer boundary; found by the oracle harness. Re-record the six directory-scan goldens that had captured the truncation. Prepared with AI assistance (Claude Code). * Oracle: normalize the hook-admin command in both runtimes' forms and audit chars Prepared with AI assistance (Claude Code). * Skill text: invoke the impeccable launcher instead of node scripts Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>` (context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points Windows shells without sh at impeccable.cmd, and says the launcher runs a self-contained binary. allowed-tools follows. Prepared with AI assistance (Claude Code). * Scripts dir: replace the Node scripts with the impeccable launcher skill/scripts keeps command-metadata.json and the page JS; every .mjs entry point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION), scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir. Prepared with AI assistance (Claude Code). * Build: ship the launcher instead of bundling the JS engine readSourceFiles no longer copies cli/engine into the skill; the scripts payload is the launcher (executable bit preserved through dist, plugin/, and universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every build), the page JS, and command-metadata.json. Hook manifests call `<scripts>/impeccable hook` behind an existence guard (Codex adds a commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit; GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and systemMessage notice are gone. build:release fetches the pinned engine for every target (lenient) and stages bin/<os-arch>/ into the dist skill copies after root harness dirs and plugin/ were synced, so git-delivered trees stay launcher-only. The detection-rule count check reads the vendored extension/detector/antipatterns.json and is skipped when absent. build:browser is a stub; the codex prefix rewrite leaves `{{scripts_path}}/impeccable` alone. Prepared with AI assistance (Claude Code). * CLI: turn the impeccable npm package into a platform-binary shim cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code). * Tests: gate behavior on the oracle and the engine binary Unit tests of the deleted Node scripts and the JS detector are removed; their behavior is pinned by tests/oracle goldens (frozen JS behavior plus reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/, via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework fixture sweep drives live-inject, live-wrap, and detect-csp through the binary the same way. record.mjs learns --bin. The function-level vectors under tests/oracle/vectors/calls are committed as the frozen snapshot they can no longer be regenerated from. Suites: core trimmed to build and transformer tests, oracle added to the default run, detector/live reduced to packaging and reference checks, the live-e2e helper tests move to the opt-in live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder. Prepared with AI assistance (Claude Code). * 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). * Tests: tighten the hook command guard assertion Prepared with AI assistance (Claude Code). * Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md Prepared with AI assistance (Claude Code). * Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). * Tests: drive the live-e2e orchestrator through the engine binary The session, fake-agent loop, steer test, and manual-edit probe spawn <binary> <verb> (live-server, live, live-inject, live-wrap, live-insert, live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs instead of node skill/scripts/live-*.mjs; the completion typing the agent imported from the deleted live/completion.mjs is a small local helper. The live-e2e helper unit tests move back into the default live suite (the steer loop skips without a binary). Prepared with AI assistance (Claude Code). * Tests: run new-work-e2e through the engine's serve-question and generate-image verbs Prepared with AI assistance (Claude Code). * Tests: point the skill-behavior harness at the launcher and engine binary The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs without a download; scenarios assert on 'impeccable context' instead of context.mjs and skip without a binary. Prepared with AI assistance (Claude Code). * Tests: note what plugin-e2e validates before and after the generated-output sync Prepared with AI assistance (Claude Code). * Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta Prepared with AI assistance (Claude Code). * Rebase reconciliation: fold main's post-freeze work into the swapped tree The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). * Oracle: restore detector goldens to post-fix behavior after the engine ports The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid pass, comment stripping, root-relative linked stylesheets, URL userinfo redaction, inert ignore-value refusal) and the comp-fidelity rules organic-clip-path / buried-raster are ported to the engine. Re-records the gap-pinning detect goldens from the fixed binary (glow.html included: its .photo-opaque-grad column now carries the buried-raster finding it was written for), replays the frozen checkHtmlPatterns call vectors through the last JS engine state in history (db1462b9^; args untouched, 14 of 101 results moved), and rewrites the DELTAS gap section into the landed-ports note. Each re-recorded json fixture golden byte-matches that JS state's output; oracle: 770 pass, 0 fail. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope, end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 + 3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and deep findings keep their established artifact order, 80997663). Re-recorded goldens whose old bytes froze pre-fix behavior, with a DELTAS.md entry naming each upstream hash: the Stop finding-cache sync (3c442af7), the Edit|Write manifests without the retired MultiEdit matcher (7d5c60d2), and the failWithRollback field order (1f2c3f9d). Prepared with AI assistance (Claude Code). * Oracle: drop a duplicated DELTAS section The verb-fix section landed twice when two porting sessions staged the same file; keep one copy. Prepared with AI assistance (Claude Code). * Oracle: pin the hooks ignore-value inert-entry refusal Three hadmin-ignore-value-inert-* cases record the engine's port of be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose findings can never extract one is refused with the wildcard-plus-file route (and no config write), while the wildcard scoped form for the same rule is accepted. Goldens recorded from the engine binary and verified byte-for-byte against the ea360025 hook-admin.mjs on the same sequences. No existing golden changes, so no DELTAS entry is owed. Prepared with AI assistance (Claude Code). * Launcher: fail closed on a missing download checksum (engine triage C1) Byte-identical sync of the engine repo's launchers: a freshly downloaded engine binary now runs only after verifying against its .sha256 sidecar. A sidecar that cannot be fetched, or a machine with no sha256 tool, refuses the download instead of exec'ing an unverified binary; the wget-only path fetches the sidecar too. Binaries already on PATH or in the cache that pass engine-probe are unaffected. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Enforce engine-before-skill release order (triage D4) The launcher, npm shim, and `impeccable install` all resolve the engine binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap merge published ahead of the engine release + platform packages dead-ends every install path. Add a mechanical guard: - scripts/check-engine-release.mjs: verifies all five dist binaries + .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero. Honors IMPECCABLE_DOWNLOAD_BASE. - release.mjs: hard-fails release:skill and release:cli when assets are missing; extension is exempt (vendored WASM detector, no engine exec). - CI engine-release-ready job: runs the check, continue-on-error with a loud ::warning until the first engine release exists (flip to false then). - CLAUDE.md Releases: documents the enforced ordering. Prepared with AI assistance (Claude Code). * Oracle: re-record the Sep-1 verb fixes ported to the Rust engine Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4. - detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps): the #678 overused-font primary-face change (a system stack keeps its system face, so a Roboto fallback no longer flags). Upstream 2cfd6076. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: fix pre-existing release-guard staging on the swap branch release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * oracle: pin E8 stale-hook-manifest detector fallback (context) Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: stop two harness hangs from wedging a whole run Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). * launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix) Prepared with AI assistance (Claude Code). * Node-free swap: comp-fidelity verbs move to the engine The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase) and their six libs are ported into the impeccable-engine binary. This removes the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'` now returns nothing. - reference/new-work.md, reference/visualize.md, and the asset-producer / finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead of `node <script>.mjs`. - Deleted the ten ported .mjs and the four JS unit tests that imported them (their behavior is now covered by the engine's Rust tests and the oracle); removed those files from scripts/test-suites.mjs. - Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic workspace, recorded from the engine binary; the deterministic outputs are byte-identical to the JS the scripts left behind. - docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and the runtime-resolved (never-committed) font-index catalog. The font-index catalog JSON stays shipped in the skill (data/font-index.json); the engine resolves it at run time and never vendors it. Prepared with AI assistance (Claude Code). * reorg: public plumbing for the in-repo Rust workspace and the two-release flow The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * reorg C: the open Rust runtime joins this repo as one Cargo workspace The engine no longer lives in a separate repo. `crates/` is a snapshot of the open crates (foundation, core, common, context, live, hook, skills, comp, comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git archive of the engine repo at the commit that finished the boundary split. None of that repo's history comes with it, and none of it should: the closed half stays private. The closed half is the rule engine. It ships as a prebuilt native archive per target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub Release on this repo. `crates/core/build.rs` resolves and links it three ways: `IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the `~/.impeccable/detector/<version>/<target>/` cache, else a download verified against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol C ABI; nothing above it knows the boundary exists. What changed versus the engine repo copy: - Every crate manifest moves from `license-file.workspace` to `license.workspace` (this workspace declares Apache-2.0), and the workspace gains the `postcard` dependency the boundary encoding needs. - The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of a sibling `launcher/` dir, and `engine_binary` downloads from `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead of the retired dist repo. No oracle golden carried the old URL, so no re-recording was owed. - The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`, `../impeccable-second` or a hardcoded home directory now resolve the root as `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an override for an out-of-tree checkout. - The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm glue) is no longer tracked. `crates/core/build.rs` resolves it beside the archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and live mode serves that. `scripts/check-detector-release.mjs` now requires it and its `.sha256` in a detector release. - The live crate embeds `skill/scripts/live-browser*.js` and `modern-screenshot.umd.js` directly rather than through vendored copies, so the binary and the installed skill cannot drift. - `crates/browser/assets/` (an unused second copy of the bundle) is gone. - `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a plain `cargo build --release -p impeccable` is enough to run `bun run test`. Verified with the archive from a local detector build: `cargo test --workspace` 267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the default suite green, and the launcher's `engine-probe` handshake answering through `skill/scripts/impeccable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime They describe the binary's environment contract and the parity method every crate here was ported with; both belong next to the crates now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core/build.rs: refuse a detector archive built by another rustc, in plain words The archive links only against the exact rustc that built it; a mismatch used to surface as pages of undefined std symbols from the linker. The detector repo now writes rustc-version.txt next to the archive (and ships it with the release); when it is present, build.rs compares it with its own compiler and names both versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * build:extension: ship the wasm-core extension shell and vendor its detector from the detector release `bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: replay byte-for-byte on Linux too The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json') Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: darwin-x64 builds on macos-14 (macos-13 is retired) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Open the detector: the rule crates join the workspace, the C-ABI goes away The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core: doc comments drop the open/closed split The rule crate and the foundation crate are both Apache-2.0 in one workspace now, so "open", "closed" and "crosses the boundary" no longer describe anything. Comments only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Rule packs: downstream crates add rules on all three engines; wasm detect surface A crate that depends on this workspace can now add rules without forking it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync + Debug) carries a pack's registry rows plus three hooks that default to empty: `check_text` for the text engine, `check_element_dom` and `check_page_dom` for the browser driver. `impeccable_html::StaticRulePack` adds `check_document` for the static engine, where the document model belongs to the html crate and detect cannot name it. The registry keeps ANTIPATTERNS as the built-in list; `registry::extend` appends a pack's rows and every lookup consults them after the built-ins, so a pack can never shadow a built-in id (extend panics on a collision and is idempotent per slice). `all_antipatterns()` is the built-ins followed by the registered rows. Hook order, chosen so built-in output cannot move: - detect_text: after every matcher, analyzer and the dedupe, before inline ignores, so `impeccable-disable` waives pack rules like built-in ones. - detect_html_source: after the element rules, the design-system merge and the page passes, again before inline ignores. One pack pass per HTML file: the document hook when set, otherwise the text hook over the raw source, so a pack implementing both never reports twice. - collect_browser_findings: the element hook at the end of the per-element loop through the same disabled-rules filter and group, the page hook after every built-in page pass with the same el-or-body attribution. A pack travels on TextOptions / ScanOptions, DetectHtmlOptions (static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig (serde-skipped: a pack is a Rust value, not JSON from the page). The shipped binary installs none. `crates/wasm --features detect` exposes the two file engines as JSON exports for hosts that cannot exec the binary: `detect_text_json` and `detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`, returning the findings array `detect --json` prints. `antipatterns_json` now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are Rust-only, for a crate that links this one as an rlib. Tests: registry extension and collision in foundation, one test pack per engine (crates/core, crates/detect, crates/html tests) proving each hook fires, that the built-in findings are unchanged, and that the waivers and the disabled-rules list cover pack rules, plus the wasm export shapes. Workspace tests 346 to 361, oracle 795/0 unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist under the open design Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * bundle: the page JS and the bundler become a library crate downstream packs can reuse The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * The immediate tier moves to the registry, and reaches wasm The design hook's immediate-tier list is the set of rule ids worth fixing at the edit site, and a downstream reviewer wants the same set to decide how loudly a finding is reported. `impeccable-hook` is native-only, so the list moves to `impeccable_core::registry` (the hook re-exports it) and the `detect` feature gains `immediate_tier_rules_json()`. The export is behind `detect`, which the in-page bundle does not build, so the tracked browser asset is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: Pristine tracks the engine by revision pin, not npm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist is maintainer-side, not part of the tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix flat type hierarchy false positives (#702) Upstream sha 84728e9ce43a3dba2a453b20130bbf836190d77c. The rule now reads rendered semantic roles and the dominant size per role instead of the raw set of font sizes on the page, and it fires only when every adjacent role step is under 1.25x. - crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES / MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and checkFlatTypeHierarchySamples, the shared half of checks.mjs. - crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over the Dom trait, with the overlay skip selector checkTypography passes. - crates/html page.rs gets the same walk over StaticDocument. - crates/detect drops the source-only analyzer: flat-type-hierarchy leaves REGEX_ANALYZERS, the text-content analyzers shift to index 1, and analyzer_rule_id loses its first row. - crates/html cascade defaults gain contentVisibility, and crates/foundation registry carries the reworded description. Goldens re-recorded (the binary now matches origin/main's JS engine on every one of these fixtures, verified by scanning the shared corpus with both): glow, icon-tile-stack, layout, modern-color-borders, motion, named-color-borders, numbered-section-markers, oklch-neon-text, typography-should-flag, json and text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix detector URL scans and advisory handling (#709) Upstream sha fa44839f7289fced3f51946684656a28775638cc. Advisory handling. `severity` becomes the canonical registry field: the `advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on `severity == "advisory"`, and `derive_advisory_flag` stamps the finding's `advisory: true` from the effective severity, so a per-finding promotion or demotion carries the flag. The html and browser engines call it after their severity override; the detect CLI and the hook accept either spelling; the driver's serializer and the wasm registry exports derive it the same way. em-dash-overuse moves from `advisory: true` to `severity: "advisory"`. URL scans. `expand_joined_url_targets` splits an argv value that is entirely whitespace-separated URLs and leaves paths with spaces alone. The browser driver reads the readable linked-stylesheet corpus into the HTML pattern corpora and resolves a finding's selector with `selector_nodes_for_live_dom` / `pseudo_element_host_selector`, so an unresolvable selector drops the finding instead of keeping it page-level. The CSSOM walk itself is page JS: `browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping rules flattened, container-query probes, effective keyframes) and puts it in the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page route, and the Dom trait carries `linked_stylesheet_text`. Also `enclosing_css_selector` blanks comments before hunting the previous declaration delimiter, and `check_typography` reports the uniquely most-used family instead of every family over a 15% share. Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns` is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree over the shared corpus (432 findings). The two changed lines in tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were re-recorded by running origin/main's `checkHtmlPatterns` over the frozen args; only the comment-polluted selector changed. Goldens re-recorded for the advisory partition (config-*, fixture gemini/gpt-tells, numbered-section-labels, scoped-ignore, shape-assembled-illustration, color, em-dash-entities) and the help text, each cross-checked against the JS on origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707) Upstream sha 32b270f4e8ec0af40ef85508c224f0f49096bd7d. `find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex` crate has no lookahead, so the maximal digit run plus the word boundary is matched as before and the byte after it is tested for `/`. The text engine gains the JS-source scanner (`scan_js`) and the scope helpers on top of it: `containing_markup_tag` keeps a gray text class from pairing with a background in a sibling tag on the same line, and `find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b` class expression into its arms, recursing into nested ternaries, ignoring `?.` and `??`, and keeping a common prefix and post-ternary suffix in every arm. `MatchCtx` now carries the match offset the scope lookup needs. Verified against origin/main's JS: all eleven cases from the upstream test file plus a nested / nullish / suffix set produce byte-identical findings on both engines; they are pinned as Rust unit tests in `regex_matchers` and `checks::rules`. The shared fixture corpus stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: resolve unique --target names in monorepos (#706) Upstream sha 8b326fc81e026fffbcdcecd94ce34af956ebea79. `resolve_target_path` / `find_unique_bare_target` in `crates/context`: a `--target` that does not exist and reduces to a single path segment under cwd resolves to the one workspace candidate with that name, so `--target a` selects `apps/a`. A caller that already absolutized the name against cwd (live and the other helpers do) takes the same route. Ambiguous or unknown names still report the miss. The context CLI resolves the target once and hands the resolved path to `load_context`, replacing `path_exists_for_target`. Oracle: four new `context-monorepo-target-bare-*` cases (bare name, absolutized bare name, unknown name, bare name from a child cwd). `context-monorepo-target-b-inherits` was re-recorded: resolving the target before `load_context` changes its `surfaceBriefReason` from `not-found` to `invalid-target`, which is what origin/main's `context.mjs` prints for the same run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Next.js 16 CSP and parent hook discovery (#710) Upstream sha 672ca29642b513bc3365afb0e309fff3d6dfa752. CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook beside `middleware.*`, but only where it sits at a project root or its `src/` directory: the scan root itself, or a nested directory carrying a Next project marker (a `next.config.*`, an `app` / `pages` dir, or a `next` dependency). A same-named helper elsewhere in the tree is not the framework hook. Context. `find_git_boundary_root` gives `resolve_project` a git-boundary notion: an explicit target inside its own repository resolves against that repository, and an external target resolves against its own root, so caller context never leaks across the boundary. `hook_manifest_search_roots` replaces the cwd/projectRoot/repoRoot triple with a walk up from the project root that stops at the first git boundary, and each root's own hook lifecycle config is honored before its manifest counts as coverage. Verified against origin/main's JS: nine `detect-csp` placements and five hook-discovery scenarios (enclosing harness root, that root disabled, sibling target, nested git target, markerless nested git target) produce identical output. Oracle: five `csp-proxy-*` cases and five `context-hook-*` / `context-markerless-nested-git-target` cases. Four route-target goldens were re-recorded because #710 resolves a `/`-prefixed target outside the workspace; each was cross-checked against origin/main, and `surface-brief-write-route` has a DELTAS entry for the one wording difference (an unwritable filesystem root). `tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from the merge importing the deleted `detectCsp`; it now drives `detect-csp` through the binary like the rest of that file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: fail URL scans when the browser is unavailable (#711) Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: OpenCode slash command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78. OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a pinned skill never reaches its slash menu. `pin` now writes `commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and skips `.opencode` in the SKILL.md loop so no unreachable `.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded, and reaches both scopes even when the skill itself is gone. `find_opencode_commands_dirs` covers the project-local dir when the project has the skill and the user config dir when Impeccable is installed globally, resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`). The build-tooling half of the upstream change (transformers, the OpenCode command the build generates, `root-commands-sync`) came in with the merge and needed no port. Verified against origin/main's pin.mjs across seven scenarios (no harness, project scope, user scope, a foreign command file, pin then unpin, unpin over a foreign file, unpin with nothing pinned): identical stdout, identical file sets, identical file contents apart from the one deliberate difference. Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body naming the launcher rather than `node .../context.mjs`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Codex skill version metadata (#703) Upstream sha 482368511ace07982a7cd3a23dd60cf62d6f68c8. Codex's validator rejects unknown top-level keys, so the Codex and `.agents` skills now carry `version` under the spec-defined `metadata:` map. Both version readers learn the same parser: `parse_skill_frontmatter_version` in `crates/context` (the boot update check) and `extract_version` in `crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy top-level one still reads, only the map's own indent level counts, tabs count as two spaces, and a comment line is skipped. The build-tooling half (`versionInMetadata` on the two providers, the YAML emitter's nested-object branch) came in with the merge. Fourteen frontmatter shapes were recorded from origin/main's `parseSkillFrontmatterVersion` and pinned as unit tests in both crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix skill subcommand help handling (#708) Upstream sha a26419917716b16623cc830429f3cc1a4f7cd630. `install`, `link`, `update` and `check` render static help before entering any operational path, through both the top-level verb and the legacy `skills` namespace, for `--help` and `-h` alike. Verified against origin/main's `cli/bin/cli.js`: all six spellings produce identical text and exit codes. Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the help paths are pinned there; every other installer path writes into harness directories or reaches the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle: goldens for the three fixtures the merge added `tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: the installer half of the OpenCode command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78, the part of it that lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`. `copy_provider_commands` mirrors `copy_provider_skills` for a provider's compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`, user scope writes the config dir OpenCode actually scans (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`), and a pre-#406 global install at `~/.opencode/commands/` loses exactly the files just written while siblings, symlinked dirs and home-rooted git repos are left alone. It runs on install, on the reinstall refresh, on update, and on link, which is the only path that can deliver the bridge to a linked install. `is_up_to_date` now compares the bundle's command files too, so an install whose skills match but whose bridge is missing or drifted refreshes instead of reporting success while the slash command stays absent. Only bundle-shipped files are compared, so a pinned shortcut never affects freshness. `tests/copy-provider-commands.test.js` arrived with the merge importing the deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to `crates/skills/tests/provider_commands_tests.rs` (project scope, the three user-scope dir resolutions, the legacy migration and its two guards, a provider with no commands dir, and the four `isUpToDate` command-awareness cases), and the file is removed and deregistered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CI: the first full run on the branch, three fixes - The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context tests: the verbatim-prefix strip spells the prefix once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: derive the snapshot identity from the verb's own resolver Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: JSON-quote the snapshot identity, as the verb does Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * detect test: import resolution against platform-form paths Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills tests pass on Windows The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the kernel takes a verbatim path literally, so every `/`-joined path built under them was an invalid filename. Strip it the way Node's `realpathSync` does. The manifest, artifact and sibling-binary expectations hard-coded POSIX separators for paths the product joins with the host's semantics; derive them from `jsp::join` instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests pass on Windows Same verbatim-prefix strip on the test temp roots, plus expectations derived from the helpers the product uses: cache keys and scan targets from `jsp::join`, the config path in an admin message from the same relative form `path.relative` renders, and the footer hints from `quote_command_arg`, which deliberately switches to the double-quoted Windows form (#476 / #533). The env lock no longer poisons the sibling tests when one of them fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: html oracle goldens compare on Windows The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then render the remainder with `/` so a Windows checkout's backslashes are not read as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: widen the live read-deadline test's margin Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system timer while the crate's tests run in parallel, so the later request takes its turn later there. The bound stays far under the 60s read timeout a deadline-less read would hold the ticket for, so the test still distinguishes the fix from the regression. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests derive the rest of the host path forms The test temp helper's `write` returned a `PathBuf::join` result, which keeps the `/` inside the relative part and so does not match what the hook resolves a relative target to on Windows. Three more admin messages and the cache-root slug pinned the POSIX spelling of paths the product renders with the host's semantics (`path.resolve` also prefixes the current drive there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills test fixtures name USERPROFILE, and the win32 quoted form `os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only HOME sent the global installs into the runner's real profile. The Windows hook command carries the JSON-quoted path, so a host path's backslashes arrive escaped; derive the expectation instead of pinning the POSIX spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the oracle fixtures out with LF A finding's snippet carries the scanned file's own bytes, and the goldens were recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the grok global-install manifests as JSON The Windows hook command carries the JSON-quoted launcher path, so the path's backslashes are escaped once inside the command and again by the manifest file itself. Read the manifest as JSON and look for either quoting form instead of counting escaping layers in a raw substring match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * npm shim: refuse a download with no verifiable sidecar The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle fixture: declare the vite plugin the web workspace imports `live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Vectors: drop the 12,208 byte-identical repeat lines The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Fix: restore the live overlay's disabledValues waivers in the engine The JS engine applied value-level ignore waivers at the tail of collectBrowserFindings: `_disabledValues` read the entries the live overlay resolved for the page (skill/scripts/live-browser-ignores.js sends them as config.disabledValues), and filtered the assembled findings by the value each one reported, with design-system-color compared by color value rather than by spelling so a hex waiver suppressed a finding the browser reported as rgb(...). The Rust port dropped that stage: `disabledValues` appeared nowhere in the workspace or in browser-bundle, so a project entry like [detector] ignoreValues = [{ rule = "overused-font", value = "geist mono" }] stopped reaching the overlay. The rules the CLI and the edit hook waive kept drawing markers and counting toward the badge. Restore it end to end: * BrowserConfig gains `disabled_values`, parsed leniently so a hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped rather than failing the whole config, the way the JS filter did. * The driver applies the waivers after every pass, so a rule pack's findings are covered the same way the built-in ones are, honoring the entries only in extension mode exactly as the JS read them. The normalizer, the value extractor (including the rule that bounce-easing without a direct ignoreValue offers no value) and the hex/rgb color key are ported alongside it. * collectConfigJson in the in-page bundle and configJson in the offscreen bundle forward the field. The extension never sends it, so its behavior is unchanged. Coverage: two driver unit tests (suppression by font value, by hex waiver across the rgb spelling, and the extension-mode gate; plus the config parse and the normalizers), a skipScan test that pins the empty shape for every stage the core produces, and crates/wasm/tools/disabled-values-check.mjs, a browser-backed check ported from the retired tests/detect-antipatterns-browser.test.mjs case that the swap left without a replacement. Against the previous bundle it fails on exactly the three waiver assertions and passes the skipScan one, which is the shape of the regression. Two related review findings were checked and are not defects. skipScan is gated on extension mode in both the driver and the bundle, which is what the JS did (index.mjs#skipScanActive), and the live overlay runs in extension mode: live-browser.js sets `s.dataset.impeccableExtension` on the injected /detect.js tag, and the overlay's whole detect toggle travels over the postMessage loop that 50-scan.js installs only under EXTENSION_MODE. The visual contrast stage is not leaking either: collectBrowserFindingsAsync and scan() both consult skipScanActive(), and the offscreen path skips its visual pass on config.skipScan. The tracked live asset is regenerated (cargo xtask bundle). The oracle replays with zero unreviewed differences: the new field defaults empty and the filter is inert without it, and no CLI path sets extension mode. AI-assisted change: implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Shim test: run from a staged copy and prove the download happened The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the loader now hands off when the resume is the arrival The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live server: stop ends the process, SSE skips the mutation lane Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the shader teardown can no longer race its own construction The new cycling assertion caught a real defect on CI: vite8-react-insert reached CYCLING with #impeccable-live-shader still painted over the page. showShaderOverlay is async. It appends its canvas synchronously, then awaits createImageBitmap and finishes the GL setup before it publishes shaderState. hideShaderOverlay returned early on a null shaderState, so a teardown that landed inside that window did nothing, and the construction then published itself over a session that had already left GENERATING, with no teardown left to run. The scroll tick kept repositioning it, which is why the CI page.html shows the canvas sized from the capture rect but styled to the cycling anchor. Every teardown now bumps a shader epoch before it does anything else, and a construction pins the epoch it owns and abandons its canvas (releasing the GL context) at every point past an await and before any publish, including both bitmap-fallback publishes. A teardown also drops a shader node that no shaderState owns, so an already-orphaned canvas cannot survive one. Reproduced by widening the append-to-publish window: with a 400ms delay after uiAppend, vite8-react-insert failed with the CI error and the probe showed the teardown arriving at CYCLING with shaderState still null. The same run passes with this change, as does a 1500ms window on insert and plain. Locally that window is about 4ms, which is why it only showed on a slower runner. The four remaining setLiveState('CYCLING') sites that did not lower the loader now do: the SSE done handler (the one route that can reach CYCLING from GENERATING), the Svelte republish remount, and the two accept failure recoveries. The e2e assertion already waits up to 5s for the shader to clear, so it was never racing a legitimate teardown; it is left as it is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: every active-session wrapper lookup goes through the resolver Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: a discard releases every wrapper it hid Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Release: publish the npm platform packages in one command bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: pin checkout, upload-artifact and download-artifact at v7 The v4 pins target Node 20, which the runner now deprecates and forces onto Node 24 with a warning on every step. The rest of the workflows already use v7. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: make the temp-dir helpers unique under a coarse clock Windows' system clock is coarse enough that two parallel tests could get the same pid-plus-nanoseconds directory name and then remove each other's files (rust-windows: close_verb_round_trip_and_ownership, NotFound). A per-process counter is appended to the name. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: declare the temp-dir counter in the hook cache-root tests The previous commit referenced TMP_SEQ there without defining it. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-09-04 10:42:45 -07:00
"cli/bin/",
"LICENSE"
],
"scripts": {
2026-06-08 16:54:11 -07:00
"build:skills": "bun run scripts/build.js --skip-root-sync",
"build:skills:release": "bun run scripts/build.js",
"build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:extension": "node scripts/build-extension.js",
"package:vscode": "bun run build:skills && cd dist/vscode && bunx --package @vscode/vsce@3.9.2 vsce package --no-dependencies",
The Rust engine: one binary replaces every script and the JS detector, fully open (#714) * Add oracle harness: verb goldens and function-level vectors Records stdout/stderr/exit/files for every impeccable verb over a fixed corpus and replays them against an alternate implementation. Adds a loader hook that captures per-function call vectors from the pure engine modules. Prepared with AI assistance (Claude Code). * Oracle: hook, hook-before-edit, hook-admin cases and goldens Prepared with AI assistance (Claude Code). * Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb Prepared with AI assistance (Claude Code). * Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens Prepared with AI assistance (Claude Code). * Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon) Prepared with AI assistance (Claude Code). * Oracle: mask the binary path before HOME; export launcher env to the binary Prepared with AI assistance (Claude Code). * detect: set process.exitCode instead of exiting after the final write process.exit() right after a large piped stdout write truncated JSON output at the pipe buffer boundary; found by the oracle harness. Re-record the six directory-scan goldens that had captured the truncation. Prepared with AI assistance (Claude Code). * Oracle: normalize the hook-admin command in both runtimes' forms and audit chars Prepared with AI assistance (Claude Code). * Skill text: invoke the impeccable launcher instead of node scripts Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>` (context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points Windows shells without sh at impeccable.cmd, and says the launcher runs a self-contained binary. allowed-tools follows. Prepared with AI assistance (Claude Code). * Scripts dir: replace the Node scripts with the impeccable launcher skill/scripts keeps command-metadata.json and the page JS; every .mjs entry point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION), scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir. Prepared with AI assistance (Claude Code). * Build: ship the launcher instead of bundling the JS engine readSourceFiles no longer copies cli/engine into the skill; the scripts payload is the launcher (executable bit preserved through dist, plugin/, and universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every build), the page JS, and command-metadata.json. Hook manifests call `<scripts>/impeccable hook` behind an existence guard (Codex adds a commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit; GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and systemMessage notice are gone. build:release fetches the pinned engine for every target (lenient) and stages bin/<os-arch>/ into the dist skill copies after root harness dirs and plugin/ were synced, so git-delivered trees stay launcher-only. The detection-rule count check reads the vendored extension/detector/antipatterns.json and is skipped when absent. build:browser is a stub; the codex prefix rewrite leaves `{{scripts_path}}/impeccable` alone. Prepared with AI assistance (Claude Code). * CLI: turn the impeccable npm package into a platform-binary shim cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code). * Tests: gate behavior on the oracle and the engine binary Unit tests of the deleted Node scripts and the JS detector are removed; their behavior is pinned by tests/oracle goldens (frozen JS behavior plus reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/, via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework fixture sweep drives live-inject, live-wrap, and detect-csp through the binary the same way. record.mjs learns --bin. The function-level vectors under tests/oracle/vectors/calls are committed as the frozen snapshot they can no longer be regenerated from. Suites: core trimmed to build and transformer tests, oracle added to the default run, detector/live reduced to packaging and reference checks, the live-e2e helper tests move to the opt-in live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder. Prepared with AI assistance (Claude Code). * 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). * Tests: tighten the hook command guard assertion Prepared with AI assistance (Claude Code). * Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md Prepared with AI assistance (Claude Code). * Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). * Tests: drive the live-e2e orchestrator through the engine binary The session, fake-agent loop, steer test, and manual-edit probe spawn <binary> <verb> (live-server, live, live-inject, live-wrap, live-insert, live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs instead of node skill/scripts/live-*.mjs; the completion typing the agent imported from the deleted live/completion.mjs is a small local helper. The live-e2e helper unit tests move back into the default live suite (the steer loop skips without a binary). Prepared with AI assistance (Claude Code). * Tests: run new-work-e2e through the engine's serve-question and generate-image verbs Prepared with AI assistance (Claude Code). * Tests: point the skill-behavior harness at the launcher and engine binary The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs without a download; scenarios assert on 'impeccable context' instead of context.mjs and skip without a binary. Prepared with AI assistance (Claude Code). * Tests: note what plugin-e2e validates before and after the generated-output sync Prepared with AI assistance (Claude Code). * Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta Prepared with AI assistance (Claude Code). * Rebase reconciliation: fold main's post-freeze work into the swapped tree The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). * Oracle: restore detector goldens to post-fix behavior after the engine ports The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid pass, comment stripping, root-relative linked stylesheets, URL userinfo redaction, inert ignore-value refusal) and the comp-fidelity rules organic-clip-path / buried-raster are ported to the engine. Re-records the gap-pinning detect goldens from the fixed binary (glow.html included: its .photo-opaque-grad column now carries the buried-raster finding it was written for), replays the frozen checkHtmlPatterns call vectors through the last JS engine state in history (db1462b9^; args untouched, 14 of 101 results moved), and rewrites the DELTAS gap section into the landed-ports note. Each re-recorded json fixture golden byte-matches that JS state's output; oracle: 770 pass, 0 fail. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope, end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 + 3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and deep findings keep their established artifact order, 80997663). Re-recorded goldens whose old bytes froze pre-fix behavior, with a DELTAS.md entry naming each upstream hash: the Stop finding-cache sync (3c442af7), the Edit|Write manifests without the retired MultiEdit matcher (7d5c60d2), and the failWithRollback field order (1f2c3f9d). Prepared with AI assistance (Claude Code). * Oracle: drop a duplicated DELTAS section The verb-fix section landed twice when two porting sessions staged the same file; keep one copy. Prepared with AI assistance (Claude Code). * Oracle: pin the hooks ignore-value inert-entry refusal Three hadmin-ignore-value-inert-* cases record the engine's port of be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose findings can never extract one is refused with the wildcard-plus-file route (and no config write), while the wildcard scoped form for the same rule is accepted. Goldens recorded from the engine binary and verified byte-for-byte against the ea360025 hook-admin.mjs on the same sequences. No existing golden changes, so no DELTAS entry is owed. Prepared with AI assistance (Claude Code). * Launcher: fail closed on a missing download checksum (engine triage C1) Byte-identical sync of the engine repo's launchers: a freshly downloaded engine binary now runs only after verifying against its .sha256 sidecar. A sidecar that cannot be fetched, or a machine with no sha256 tool, refuses the download instead of exec'ing an unverified binary; the wget-only path fetches the sidecar too. Binaries already on PATH or in the cache that pass engine-probe are unaffected. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Enforce engine-before-skill release order (triage D4) The launcher, npm shim, and `impeccable install` all resolve the engine binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap merge published ahead of the engine release + platform packages dead-ends every install path. Add a mechanical guard: - scripts/check-engine-release.mjs: verifies all five dist binaries + .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero. Honors IMPECCABLE_DOWNLOAD_BASE. - release.mjs: hard-fails release:skill and release:cli when assets are missing; extension is exempt (vendored WASM detector, no engine exec). - CI engine-release-ready job: runs the check, continue-on-error with a loud ::warning until the first engine release exists (flip to false then). - CLAUDE.md Releases: documents the enforced ordering. Prepared with AI assistance (Claude Code). * Oracle: re-record the Sep-1 verb fixes ported to the Rust engine Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4. - detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps): the #678 overused-font primary-face change (a system stack keeps its system face, so a Roboto fallback no longer flags). Upstream 2cfd6076. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: fix pre-existing release-guard staging on the swap branch release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * oracle: pin E8 stale-hook-manifest detector fallback (context) Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: stop two harness hangs from wedging a whole run Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). * launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix) Prepared with AI assistance (Claude Code). * Node-free swap: comp-fidelity verbs move to the engine The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase) and their six libs are ported into the impeccable-engine binary. This removes the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'` now returns nothing. - reference/new-work.md, reference/visualize.md, and the asset-producer / finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead of `node <script>.mjs`. - Deleted the ten ported .mjs and the four JS unit tests that imported them (their behavior is now covered by the engine's Rust tests and the oracle); removed those files from scripts/test-suites.mjs. - Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic workspace, recorded from the engine binary; the deterministic outputs are byte-identical to the JS the scripts left behind. - docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and the runtime-resolved (never-committed) font-index catalog. The font-index catalog JSON stays shipped in the skill (data/font-index.json); the engine resolves it at run time and never vendors it. Prepared with AI assistance (Claude Code). * reorg: public plumbing for the in-repo Rust workspace and the two-release flow The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * reorg C: the open Rust runtime joins this repo as one Cargo workspace The engine no longer lives in a separate repo. `crates/` is a snapshot of the open crates (foundation, core, common, context, live, hook, skills, comp, comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git archive of the engine repo at the commit that finished the boundary split. None of that repo's history comes with it, and none of it should: the closed half stays private. The closed half is the rule engine. It ships as a prebuilt native archive per target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub Release on this repo. `crates/core/build.rs` resolves and links it three ways: `IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the `~/.impeccable/detector/<version>/<target>/` cache, else a download verified against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol C ABI; nothing above it knows the boundary exists. What changed versus the engine repo copy: - Every crate manifest moves from `license-file.workspace` to `license.workspace` (this workspace declares Apache-2.0), and the workspace gains the `postcard` dependency the boundary encoding needs. - The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of a sibling `launcher/` dir, and `engine_binary` downloads from `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead of the retired dist repo. No oracle golden carried the old URL, so no re-recording was owed. - The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`, `../impeccable-second` or a hardcoded home directory now resolve the root as `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an override for an out-of-tree checkout. - The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm glue) is no longer tracked. `crates/core/build.rs` resolves it beside the archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and live mode serves that. `scripts/check-detector-release.mjs` now requires it and its `.sha256` in a detector release. - The live crate embeds `skill/scripts/live-browser*.js` and `modern-screenshot.umd.js` directly rather than through vendored copies, so the binary and the installed skill cannot drift. - `crates/browser/assets/` (an unused second copy of the bundle) is gone. - `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a plain `cargo build --release -p impeccable` is enough to run `bun run test`. Verified with the archive from a local detector build: `cargo test --workspace` 267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the default suite green, and the launcher's `engine-probe` handshake answering through `skill/scripts/impeccable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime They describe the binary's environment contract and the parity method every crate here was ported with; both belong next to the crates now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core/build.rs: refuse a detector archive built by another rustc, in plain words The archive links only against the exact rustc that built it; a mismatch used to surface as pages of undefined std symbols from the linker. The detector repo now writes rustc-version.txt next to the archive (and ships it with the release); when it is present, build.rs compares it with its own compiler and names both versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * build:extension: ship the wasm-core extension shell and vendor its detector from the detector release `bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: replay byte-for-byte on Linux too The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json') Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: darwin-x64 builds on macos-14 (macos-13 is retired) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Open the detector: the rule crates join the workspace, the C-ABI goes away The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core: doc comments drop the open/closed split The rule crate and the foundation crate are both Apache-2.0 in one workspace now, so "open", "closed" and "crosses the boundary" no longer describe anything. Comments only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Rule packs: downstream crates add rules on all three engines; wasm detect surface A crate that depends on this workspace can now add rules without forking it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync + Debug) carries a pack's registry rows plus three hooks that default to empty: `check_text` for the text engine, `check_element_dom` and `check_page_dom` for the browser driver. `impeccable_html::StaticRulePack` adds `check_document` for the static engine, where the document model belongs to the html crate and detect cannot name it. The registry keeps ANTIPATTERNS as the built-in list; `registry::extend` appends a pack's rows and every lookup consults them after the built-ins, so a pack can never shadow a built-in id (extend panics on a collision and is idempotent per slice). `all_antipatterns()` is the built-ins followed by the registered rows. Hook order, chosen so built-in output cannot move: - detect_text: after every matcher, analyzer and the dedupe, before inline ignores, so `impeccable-disable` waives pack rules like built-in ones. - detect_html_source: after the element rules, the design-system merge and the page passes, again before inline ignores. One pack pass per HTML file: the document hook when set, otherwise the text hook over the raw source, so a pack implementing both never reports twice. - collect_browser_findings: the element hook at the end of the per-element loop through the same disabled-rules filter and group, the page hook after every built-in page pass with the same el-or-body attribution. A pack travels on TextOptions / ScanOptions, DetectHtmlOptions (static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig (serde-skipped: a pack is a Rust value, not JSON from the page). The shipped binary installs none. `crates/wasm --features detect` exposes the two file engines as JSON exports for hosts that cannot exec the binary: `detect_text_json` and `detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`, returning the findings array `detect --json` prints. `antipatterns_json` now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are Rust-only, for a crate that links this one as an rlib. Tests: registry extension and collision in foundation, one test pack per engine (crates/core, crates/detect, crates/html tests) proving each hook fires, that the built-in findings are unchanged, and that the waivers and the disabled-rules list cover pack rules, plus the wasm export shapes. Workspace tests 346 to 361, oracle 795/0 unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist under the open design Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * bundle: the page JS and the bundler become a library crate downstream packs can reuse The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * The immediate tier moves to the registry, and reaches wasm The design hook's immediate-tier list is the set of rule ids worth fixing at the edit site, and a downstream reviewer wants the same set to decide how loudly a finding is reported. `impeccable-hook` is native-only, so the list moves to `impeccable_core::registry` (the hook re-exports it) and the `detect` feature gains `immediate_tier_rules_json()`. The export is behind `detect`, which the in-page bundle does not build, so the tracked browser asset is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: Pristine tracks the engine by revision pin, not npm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist is maintainer-side, not part of the tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix flat type hierarchy false positives (#702) Upstream sha 84728e9ce43a3dba2a453b20130bbf836190d77c. The rule now reads rendered semantic roles and the dominant size per role instead of the raw set of font sizes on the page, and it fires only when every adjacent role step is under 1.25x. - crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES / MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and checkFlatTypeHierarchySamples, the shared half of checks.mjs. - crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over the Dom trait, with the overlay skip selector checkTypography passes. - crates/html page.rs gets the same walk over StaticDocument. - crates/detect drops the source-only analyzer: flat-type-hierarchy leaves REGEX_ANALYZERS, the text-content analyzers shift to index 1, and analyzer_rule_id loses its first row. - crates/html cascade defaults gain contentVisibility, and crates/foundation registry carries the reworded description. Goldens re-recorded (the binary now matches origin/main's JS engine on every one of these fixtures, verified by scanning the shared corpus with both): glow, icon-tile-stack, layout, modern-color-borders, motion, named-color-borders, numbered-section-markers, oklch-neon-text, typography-should-flag, json and text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix detector URL scans and advisory handling (#709) Upstream sha fa44839f7289fced3f51946684656a28775638cc. Advisory handling. `severity` becomes the canonical registry field: the `advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on `severity == "advisory"`, and `derive_advisory_flag` stamps the finding's `advisory: true` from the effective severity, so a per-finding promotion or demotion carries the flag. The html and browser engines call it after their severity override; the detect CLI and the hook accept either spelling; the driver's serializer and the wasm registry exports derive it the same way. em-dash-overuse moves from `advisory: true` to `severity: "advisory"`. URL scans. `expand_joined_url_targets` splits an argv value that is entirely whitespace-separated URLs and leaves paths with spaces alone. The browser driver reads the readable linked-stylesheet corpus into the HTML pattern corpora and resolves a finding's selector with `selector_nodes_for_live_dom` / `pseudo_element_host_selector`, so an unresolvable selector drops the finding instead of keeping it page-level. The CSSOM walk itself is page JS: `browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping rules flattened, container-query probes, effective keyframes) and puts it in the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page route, and the Dom trait carries `linked_stylesheet_text`. Also `enclosing_css_selector` blanks comments before hunting the previous declaration delimiter, and `check_typography` reports the uniquely most-used family instead of every family over a 15% share. Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns` is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree over the shared corpus (432 findings). The two changed lines in tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were re-recorded by running origin/main's `checkHtmlPatterns` over the frozen args; only the comment-polluted selector changed. Goldens re-recorded for the advisory partition (config-*, fixture gemini/gpt-tells, numbered-section-labels, scoped-ignore, shape-assembled-illustration, color, em-dash-entities) and the help text, each cross-checked against the JS on origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707) Upstream sha 32b270f4e8ec0af40ef85508c224f0f49096bd7d. `find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex` crate has no lookahead, so the maximal digit run plus the word boundary is matched as before and the byte after it is tested for `/`. The text engine gains the JS-source scanner (`scan_js`) and the scope helpers on top of it: `containing_markup_tag` keeps a gray text class from pairing with a background in a sibling tag on the same line, and `find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b` class expression into its arms, recursing into nested ternaries, ignoring `?.` and `??`, and keeping a common prefix and post-ternary suffix in every arm. `MatchCtx` now carries the match offset the scope lookup needs. Verified against origin/main's JS: all eleven cases from the upstream test file plus a nested / nullish / suffix set produce byte-identical findings on both engines; they are pinned as Rust unit tests in `regex_matchers` and `checks::rules`. The shared fixture corpus stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: resolve unique --target names in monorepos (#706) Upstream sha 8b326fc81e026fffbcdcecd94ce34af956ebea79. `resolve_target_path` / `find_unique_bare_target` in `crates/context`: a `--target` that does not exist and reduces to a single path segment under cwd resolves to the one workspace candidate with that name, so `--target a` selects `apps/a`. A caller that already absolutized the name against cwd (live and the other helpers do) takes the same route. Ambiguous or unknown names still report the miss. The context CLI resolves the target once and hands the resolved path to `load_context`, replacing `path_exists_for_target`. Oracle: four new `context-monorepo-target-bare-*` cases (bare name, absolutized bare name, unknown name, bare name from a child cwd). `context-monorepo-target-b-inherits` was re-recorded: resolving the target before `load_context` changes its `surfaceBriefReason` from `not-found` to `invalid-target`, which is what origin/main's `context.mjs` prints for the same run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Next.js 16 CSP and parent hook discovery (#710) Upstream sha 672ca29642b513bc3365afb0e309fff3d6dfa752. CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook beside `middleware.*`, but only where it sits at a project root or its `src/` directory: the scan root itself, or a nested directory carrying a Next project marker (a `next.config.*`, an `app` / `pages` dir, or a `next` dependency). A same-named helper elsewhere in the tree is not the framework hook. Context. `find_git_boundary_root` gives `resolve_project` a git-boundary notion: an explicit target inside its own repository resolves against that repository, and an external target resolves against its own root, so caller context never leaks across the boundary. `hook_manifest_search_roots` replaces the cwd/projectRoot/repoRoot triple with a walk up from the project root that stops at the first git boundary, and each root's own hook lifecycle config is honored before its manifest counts as coverage. Verified against origin/main's JS: nine `detect-csp` placements and five hook-discovery scenarios (enclosing harness root, that root disabled, sibling target, nested git target, markerless nested git target) produce identical output. Oracle: five `csp-proxy-*` cases and five `context-hook-*` / `context-markerless-nested-git-target` cases. Four route-target goldens were re-recorded because #710 resolves a `/`-prefixed target outside the workspace; each was cross-checked against origin/main, and `surface-brief-write-route` has a DELTAS entry for the one wording difference (an unwritable filesystem root). `tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from the merge importing the deleted `detectCsp`; it now drives `detect-csp` through the binary like the rest of that file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: fail URL scans when the browser is unavailable (#711) Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: OpenCode slash command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78. OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a pinned skill never reaches its slash menu. `pin` now writes `commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and skips `.opencode` in the SKILL.md loop so no unreachable `.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded, and reaches both scopes even when the skill itself is gone. `find_opencode_commands_dirs` covers the project-local dir when the project has the skill and the user config dir when Impeccable is installed globally, resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`). The build-tooling half of the upstream change (transformers, the OpenCode command the build generates, `root-commands-sync`) came in with the merge and needed no port. Verified against origin/main's pin.mjs across seven scenarios (no harness, project scope, user scope, a foreign command file, pin then unpin, unpin over a foreign file, unpin with nothing pinned): identical stdout, identical file sets, identical file contents apart from the one deliberate difference. Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body naming the launcher rather than `node .../context.mjs`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Codex skill version metadata (#703) Upstream sha 482368511ace07982a7cd3a23dd60cf62d6f68c8. Codex's validator rejects unknown top-level keys, so the Codex and `.agents` skills now carry `version` under the spec-defined `metadata:` map. Both version readers learn the same parser: `parse_skill_frontmatter_version` in `crates/context` (the boot update check) and `extract_version` in `crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy top-level one still reads, only the map's own indent level counts, tabs count as two spaces, and a comment line is skipped. The build-tooling half (`versionInMetadata` on the two providers, the YAML emitter's nested-object branch) came in with the merge. Fourteen frontmatter shapes were recorded from origin/main's `parseSkillFrontmatterVersion` and pinned as unit tests in both crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix skill subcommand help handling (#708) Upstream sha a26419917716b16623cc830429f3cc1a4f7cd630. `install`, `link`, `update` and `check` render static help before entering any operational path, through both the top-level verb and the legacy `skills` namespace, for `--help` and `-h` alike. Verified against origin/main's `cli/bin/cli.js`: all six spellings produce identical text and exit codes. Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the help paths are pinned there; every other installer path writes into harness directories or reaches the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle: goldens for the three fixtures the merge added `tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: the installer half of the OpenCode command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78, the part of it that lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`. `copy_provider_commands` mirrors `copy_provider_skills` for a provider's compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`, user scope writes the config dir OpenCode actually scans (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`), and a pre-#406 global install at `~/.opencode/commands/` loses exactly the files just written while siblings, symlinked dirs and home-rooted git repos are left alone. It runs on install, on the reinstall refresh, on update, and on link, which is the only path that can deliver the bridge to a linked install. `is_up_to_date` now compares the bundle's command files too, so an install whose skills match but whose bridge is missing or drifted refreshes instead of reporting success while the slash command stays absent. Only bundle-shipped files are compared, so a pinned shortcut never affects freshness. `tests/copy-provider-commands.test.js` arrived with the merge importing the deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to `crates/skills/tests/provider_commands_tests.rs` (project scope, the three user-scope dir resolutions, the legacy migration and its two guards, a provider with no commands dir, and the four `isUpToDate` command-awareness cases), and the file is removed and deregistered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CI: the first full run on the branch, three fixes - The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context tests: the verbatim-prefix strip spells the prefix once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: derive the snapshot identity from the verb's own resolver Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: JSON-quote the snapshot identity, as the verb does Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * detect test: import resolution against platform-form paths Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills tests pass on Windows The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the kernel takes a verbatim path literally, so every `/`-joined path built under them was an invalid filename. Strip it the way Node's `realpathSync` does. The manifest, artifact and sibling-binary expectations hard-coded POSIX separators for paths the product joins with the host's semantics; derive them from `jsp::join` instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests pass on Windows Same verbatim-prefix strip on the test temp roots, plus expectations derived from the helpers the product uses: cache keys and scan targets from `jsp::join`, the config path in an admin message from the same relative form `path.relative` renders, and the footer hints from `quote_command_arg`, which deliberately switches to the double-quoted Windows form (#476 / #533). The env lock no longer poisons the sibling tests when one of them fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: html oracle goldens compare on Windows The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then render the remainder with `/` so a Windows checkout's backslashes are not read as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: widen the live read-deadline test's margin Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system timer while the crate's tests run in parallel, so the later request takes its turn later there. The bound stays far under the 60s read timeout a deadline-less read would hold the ticket for, so the test still distinguishes the fix from the regression. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests derive the rest of the host path forms The test temp helper's `write` returned a `PathBuf::join` result, which keeps the `/` inside the relative part and so does not match what the hook resolves a relative target to on Windows. Three more admin messages and the cache-root slug pinned the POSIX spelling of paths the product renders with the host's semantics (`path.resolve` also prefixes the current drive there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills test fixtures name USERPROFILE, and the win32 quoted form `os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only HOME sent the global installs into the runner's real profile. The Windows hook command carries the JSON-quoted path, so a host path's backslashes arrive escaped; derive the expectation instead of pinning the POSIX spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the oracle fixtures out with LF A finding's snippet carries the scanned file's own bytes, and the goldens were recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the grok global-install manifests as JSON The Windows hook command carries the JSON-quoted launcher path, so the path's backslashes are escaped once inside the command and again by the manifest file itself. Read the manifest as JSON and look for either quoting form instead of counting escaping layers in a raw substring match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * npm shim: refuse a download with no verifiable sidecar The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle fixture: declare the vite plugin the web workspace imports `live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Vectors: drop the 12,208 byte-identical repeat lines The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Fix: restore the live overlay's disabledValues waivers in the engine The JS engine applied value-level ignore waivers at the tail of collectBrowserFindings: `_disabledValues` read the entries the live overlay resolved for the page (skill/scripts/live-browser-ignores.js sends them as config.disabledValues), and filtered the assembled findings by the value each one reported, with design-system-color compared by color value rather than by spelling so a hex waiver suppressed a finding the browser reported as rgb(...). The Rust port dropped that stage: `disabledValues` appeared nowhere in the workspace or in browser-bundle, so a project entry like [detector] ignoreValues = [{ rule = "overused-font", value = "geist mono" }] stopped reaching the overlay. The rules the CLI and the edit hook waive kept drawing markers and counting toward the badge. Restore it end to end: * BrowserConfig gains `disabled_values`, parsed leniently so a hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped rather than failing the whole config, the way the JS filter did. * The driver applies the waivers after every pass, so a rule pack's findings are covered the same way the built-in ones are, honoring the entries only in extension mode exactly as the JS read them. The normalizer, the value extractor (including the rule that bounce-easing without a direct ignoreValue offers no value) and the hex/rgb color key are ported alongside it. * collectConfigJson in the in-page bundle and configJson in the offscreen bundle forward the field. The extension never sends it, so its behavior is unchanged. Coverage: two driver unit tests (suppression by font value, by hex waiver across the rgb spelling, and the extension-mode gate; plus the config parse and the normalizers), a skipScan test that pins the empty shape for every stage the core produces, and crates/wasm/tools/disabled-values-check.mjs, a browser-backed check ported from the retired tests/detect-antipatterns-browser.test.mjs case that the swap left without a replacement. Against the previous bundle it fails on exactly the three waiver assertions and passes the skipScan one, which is the shape of the regression. Two related review findings were checked and are not defects. skipScan is gated on extension mode in both the driver and the bundle, which is what the JS did (index.mjs#skipScanActive), and the live overlay runs in extension mode: live-browser.js sets `s.dataset.impeccableExtension` on the injected /detect.js tag, and the overlay's whole detect toggle travels over the postMessage loop that 50-scan.js installs only under EXTENSION_MODE. The visual contrast stage is not leaking either: collectBrowserFindingsAsync and scan() both consult skipScanActive(), and the offscreen path skips its visual pass on config.skipScan. The tracked live asset is regenerated (cargo xtask bundle). The oracle replays with zero unreviewed differences: the new field defaults empty and the filter is inert without it, and no CLI path sets extension mode. AI-assisted change: implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Shim test: run from a staged copy and prove the download happened The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the loader now hands off when the resume is the arrival The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live server: stop ends the process, SSE skips the mutation lane Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the shader teardown can no longer race its own construction The new cycling assertion caught a real defect on CI: vite8-react-insert reached CYCLING with #impeccable-live-shader still painted over the page. showShaderOverlay is async. It appends its canvas synchronously, then awaits createImageBitmap and finishes the GL setup before it publishes shaderState. hideShaderOverlay returned early on a null shaderState, so a teardown that landed inside that window did nothing, and the construction then published itself over a session that had already left GENERATING, with no teardown left to run. The scroll tick kept repositioning it, which is why the CI page.html shows the canvas sized from the capture rect but styled to the cycling anchor. Every teardown now bumps a shader epoch before it does anything else, and a construction pins the epoch it owns and abandons its canvas (releasing the GL context) at every point past an await and before any publish, including both bitmap-fallback publishes. A teardown also drops a shader node that no shaderState owns, so an already-orphaned canvas cannot survive one. Reproduced by widening the append-to-publish window: with a 400ms delay after uiAppend, vite8-react-insert failed with the CI error and the probe showed the teardown arriving at CYCLING with shaderState still null. The same run passes with this change, as does a 1500ms window on insert and plain. Locally that window is about 4ms, which is why it only showed on a slower runner. The four remaining setLiveState('CYCLING') sites that did not lower the loader now do: the SSE done handler (the one route that can reach CYCLING from GENERATING), the Svelte republish remount, and the two accept failure recoveries. The e2e assertion already waits up to 5s for the shader to clear, so it was never racing a legitimate teardown; it is left as it is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: every active-session wrapper lookup goes through the resolver Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: a discard releases every wrapper it hid Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Release: publish the npm platform packages in one command bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: pin checkout, upload-artifact and download-artifact at v7 The v4 pins target Node 20, which the runner now deprecates and forces onto Node 24 with a warning on every step. The rest of the workflows already use v7. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: make the temp-dir helpers unique under a coarse clock Windows' system clock is coarse enough that two parallel tests could get the same pid-plus-nanoseconds directory name and then remove each other's files (rust-windows: close_verb_round_trip_and_ownership, NotFound). A per-process counter is appended to the name. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: declare the temp-dir counter in the hook cache-root tests The previous commit referenced TMP_SEQ there without defining it. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-09-04 10:42:45 -07:00
"fetch:engine": "node scripts/fetch-engine.mjs",
"clean": "rm -rf dist build",
"rebuild": "bun run clean && bun run build",
2026-06-08 16:54:11 -07:00
"rebuild:release": "bun run clean && bun run build:release",
"test": "node scripts/run-tests.mjs default",
"test:core": "node scripts/run-tests.mjs core",
The Rust engine: one binary replaces every script and the JS detector, fully open (#714) * Add oracle harness: verb goldens and function-level vectors Records stdout/stderr/exit/files for every impeccable verb over a fixed corpus and replays them against an alternate implementation. Adds a loader hook that captures per-function call vectors from the pure engine modules. Prepared with AI assistance (Claude Code). * Oracle: hook, hook-before-edit, hook-admin cases and goldens Prepared with AI assistance (Claude Code). * Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb Prepared with AI assistance (Claude Code). * Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens Prepared with AI assistance (Claude Code). * Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon) Prepared with AI assistance (Claude Code). * Oracle: mask the binary path before HOME; export launcher env to the binary Prepared with AI assistance (Claude Code). * detect: set process.exitCode instead of exiting after the final write process.exit() right after a large piped stdout write truncated JSON output at the pipe buffer boundary; found by the oracle harness. Re-record the six directory-scan goldens that had captured the truncation. Prepared with AI assistance (Claude Code). * Oracle: normalize the hook-admin command in both runtimes' forms and audit chars Prepared with AI assistance (Claude Code). * Skill text: invoke the impeccable launcher instead of node scripts Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>` (context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points Windows shells without sh at impeccable.cmd, and says the launcher runs a self-contained binary. allowed-tools follows. Prepared with AI assistance (Claude Code). * Scripts dir: replace the Node scripts with the impeccable launcher skill/scripts keeps command-metadata.json and the page JS; every .mjs entry point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION), scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir. Prepared with AI assistance (Claude Code). * Build: ship the launcher instead of bundling the JS engine readSourceFiles no longer copies cli/engine into the skill; the scripts payload is the launcher (executable bit preserved through dist, plugin/, and universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every build), the page JS, and command-metadata.json. Hook manifests call `<scripts>/impeccable hook` behind an existence guard (Codex adds a commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit; GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and systemMessage notice are gone. build:release fetches the pinned engine for every target (lenient) and stages bin/<os-arch>/ into the dist skill copies after root harness dirs and plugin/ were synced, so git-delivered trees stay launcher-only. The detection-rule count check reads the vendored extension/detector/antipatterns.json and is skipped when absent. build:browser is a stub; the codex prefix rewrite leaves `{{scripts_path}}/impeccable` alone. Prepared with AI assistance (Claude Code). * CLI: turn the impeccable npm package into a platform-binary shim cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code). * Tests: gate behavior on the oracle and the engine binary Unit tests of the deleted Node scripts and the JS detector are removed; their behavior is pinned by tests/oracle goldens (frozen JS behavior plus reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/, via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework fixture sweep drives live-inject, live-wrap, and detect-csp through the binary the same way. record.mjs learns --bin. The function-level vectors under tests/oracle/vectors/calls are committed as the frozen snapshot they can no longer be regenerated from. Suites: core trimmed to build and transformer tests, oracle added to the default run, detector/live reduced to packaging and reference checks, the live-e2e helper tests move to the opt-in live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder. Prepared with AI assistance (Claude Code). * 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). * Tests: tighten the hook command guard assertion Prepared with AI assistance (Claude Code). * Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md Prepared with AI assistance (Claude Code). * Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). * Tests: drive the live-e2e orchestrator through the engine binary The session, fake-agent loop, steer test, and manual-edit probe spawn <binary> <verb> (live-server, live, live-inject, live-wrap, live-insert, live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs instead of node skill/scripts/live-*.mjs; the completion typing the agent imported from the deleted live/completion.mjs is a small local helper. The live-e2e helper unit tests move back into the default live suite (the steer loop skips without a binary). Prepared with AI assistance (Claude Code). * Tests: run new-work-e2e through the engine's serve-question and generate-image verbs Prepared with AI assistance (Claude Code). * Tests: point the skill-behavior harness at the launcher and engine binary The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs without a download; scenarios assert on 'impeccable context' instead of context.mjs and skip without a binary. Prepared with AI assistance (Claude Code). * Tests: note what plugin-e2e validates before and after the generated-output sync Prepared with AI assistance (Claude Code). * Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta Prepared with AI assistance (Claude Code). * Rebase reconciliation: fold main's post-freeze work into the swapped tree The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). * Oracle: restore detector goldens to post-fix behavior after the engine ports The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid pass, comment stripping, root-relative linked stylesheets, URL userinfo redaction, inert ignore-value refusal) and the comp-fidelity rules organic-clip-path / buried-raster are ported to the engine. Re-records the gap-pinning detect goldens from the fixed binary (glow.html included: its .photo-opaque-grad column now carries the buried-raster finding it was written for), replays the frozen checkHtmlPatterns call vectors through the last JS engine state in history (db1462b9^; args untouched, 14 of 101 results moved), and rewrites the DELTAS gap section into the landed-ports note. Each re-recorded json fixture golden byte-matches that JS state's output; oracle: 770 pass, 0 fail. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope, end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 + 3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and deep findings keep their established artifact order, 80997663). Re-recorded goldens whose old bytes froze pre-fix behavior, with a DELTAS.md entry naming each upstream hash: the Stop finding-cache sync (3c442af7), the Edit|Write manifests without the retired MultiEdit matcher (7d5c60d2), and the failWithRollback field order (1f2c3f9d). Prepared with AI assistance (Claude Code). * Oracle: drop a duplicated DELTAS section The verb-fix section landed twice when two porting sessions staged the same file; keep one copy. Prepared with AI assistance (Claude Code). * Oracle: pin the hooks ignore-value inert-entry refusal Three hadmin-ignore-value-inert-* cases record the engine's port of be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose findings can never extract one is refused with the wildcard-plus-file route (and no config write), while the wildcard scoped form for the same rule is accepted. Goldens recorded from the engine binary and verified byte-for-byte against the ea360025 hook-admin.mjs on the same sequences. No existing golden changes, so no DELTAS entry is owed. Prepared with AI assistance (Claude Code). * Launcher: fail closed on a missing download checksum (engine triage C1) Byte-identical sync of the engine repo's launchers: a freshly downloaded engine binary now runs only after verifying against its .sha256 sidecar. A sidecar that cannot be fetched, or a machine with no sha256 tool, refuses the download instead of exec'ing an unverified binary; the wget-only path fetches the sidecar too. Binaries already on PATH or in the cache that pass engine-probe are unaffected. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Enforce engine-before-skill release order (triage D4) The launcher, npm shim, and `impeccable install` all resolve the engine binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap merge published ahead of the engine release + platform packages dead-ends every install path. Add a mechanical guard: - scripts/check-engine-release.mjs: verifies all five dist binaries + .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero. Honors IMPECCABLE_DOWNLOAD_BASE. - release.mjs: hard-fails release:skill and release:cli when assets are missing; extension is exempt (vendored WASM detector, no engine exec). - CI engine-release-ready job: runs the check, continue-on-error with a loud ::warning until the first engine release exists (flip to false then). - CLAUDE.md Releases: documents the enforced ordering. Prepared with AI assistance (Claude Code). * Oracle: re-record the Sep-1 verb fixes ported to the Rust engine Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4. - detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps): the #678 overused-font primary-face change (a system stack keeps its system face, so a Roboto fallback no longer flags). Upstream 2cfd6076. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: fix pre-existing release-guard staging on the swap branch release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * oracle: pin E8 stale-hook-manifest detector fallback (context) Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: stop two harness hangs from wedging a whole run Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). * launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix) Prepared with AI assistance (Claude Code). * Node-free swap: comp-fidelity verbs move to the engine The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase) and their six libs are ported into the impeccable-engine binary. This removes the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'` now returns nothing. - reference/new-work.md, reference/visualize.md, and the asset-producer / finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead of `node <script>.mjs`. - Deleted the ten ported .mjs and the four JS unit tests that imported them (their behavior is now covered by the engine's Rust tests and the oracle); removed those files from scripts/test-suites.mjs. - Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic workspace, recorded from the engine binary; the deterministic outputs are byte-identical to the JS the scripts left behind. - docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and the runtime-resolved (never-committed) font-index catalog. The font-index catalog JSON stays shipped in the skill (data/font-index.json); the engine resolves it at run time and never vendors it. Prepared with AI assistance (Claude Code). * reorg: public plumbing for the in-repo Rust workspace and the two-release flow The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * reorg C: the open Rust runtime joins this repo as one Cargo workspace The engine no longer lives in a separate repo. `crates/` is a snapshot of the open crates (foundation, core, common, context, live, hook, skills, comp, comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git archive of the engine repo at the commit that finished the boundary split. None of that repo's history comes with it, and none of it should: the closed half stays private. The closed half is the rule engine. It ships as a prebuilt native archive per target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub Release on this repo. `crates/core/build.rs` resolves and links it three ways: `IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the `~/.impeccable/detector/<version>/<target>/` cache, else a download verified against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol C ABI; nothing above it knows the boundary exists. What changed versus the engine repo copy: - Every crate manifest moves from `license-file.workspace` to `license.workspace` (this workspace declares Apache-2.0), and the workspace gains the `postcard` dependency the boundary encoding needs. - The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of a sibling `launcher/` dir, and `engine_binary` downloads from `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead of the retired dist repo. No oracle golden carried the old URL, so no re-recording was owed. - The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`, `../impeccable-second` or a hardcoded home directory now resolve the root as `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an override for an out-of-tree checkout. - The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm glue) is no longer tracked. `crates/core/build.rs` resolves it beside the archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and live mode serves that. `scripts/check-detector-release.mjs` now requires it and its `.sha256` in a detector release. - The live crate embeds `skill/scripts/live-browser*.js` and `modern-screenshot.umd.js` directly rather than through vendored copies, so the binary and the installed skill cannot drift. - `crates/browser/assets/` (an unused second copy of the bundle) is gone. - `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a plain `cargo build --release -p impeccable` is enough to run `bun run test`. Verified with the archive from a local detector build: `cargo test --workspace` 267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the default suite green, and the launcher's `engine-probe` handshake answering through `skill/scripts/impeccable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime They describe the binary's environment contract and the parity method every crate here was ported with; both belong next to the crates now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core/build.rs: refuse a detector archive built by another rustc, in plain words The archive links only against the exact rustc that built it; a mismatch used to surface as pages of undefined std symbols from the linker. The detector repo now writes rustc-version.txt next to the archive (and ships it with the release); when it is present, build.rs compares it with its own compiler and names both versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * build:extension: ship the wasm-core extension shell and vendor its detector from the detector release `bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: replay byte-for-byte on Linux too The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json') Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: darwin-x64 builds on macos-14 (macos-13 is retired) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Open the detector: the rule crates join the workspace, the C-ABI goes away The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core: doc comments drop the open/closed split The rule crate and the foundation crate are both Apache-2.0 in one workspace now, so "open", "closed" and "crosses the boundary" no longer describe anything. Comments only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Rule packs: downstream crates add rules on all three engines; wasm detect surface A crate that depends on this workspace can now add rules without forking it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync + Debug) carries a pack's registry rows plus three hooks that default to empty: `check_text` for the text engine, `check_element_dom` and `check_page_dom` for the browser driver. `impeccable_html::StaticRulePack` adds `check_document` for the static engine, where the document model belongs to the html crate and detect cannot name it. The registry keeps ANTIPATTERNS as the built-in list; `registry::extend` appends a pack's rows and every lookup consults them after the built-ins, so a pack can never shadow a built-in id (extend panics on a collision and is idempotent per slice). `all_antipatterns()` is the built-ins followed by the registered rows. Hook order, chosen so built-in output cannot move: - detect_text: after every matcher, analyzer and the dedupe, before inline ignores, so `impeccable-disable` waives pack rules like built-in ones. - detect_html_source: after the element rules, the design-system merge and the page passes, again before inline ignores. One pack pass per HTML file: the document hook when set, otherwise the text hook over the raw source, so a pack implementing both never reports twice. - collect_browser_findings: the element hook at the end of the per-element loop through the same disabled-rules filter and group, the page hook after every built-in page pass with the same el-or-body attribution. A pack travels on TextOptions / ScanOptions, DetectHtmlOptions (static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig (serde-skipped: a pack is a Rust value, not JSON from the page). The shipped binary installs none. `crates/wasm --features detect` exposes the two file engines as JSON exports for hosts that cannot exec the binary: `detect_text_json` and `detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`, returning the findings array `detect --json` prints. `antipatterns_json` now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are Rust-only, for a crate that links this one as an rlib. Tests: registry extension and collision in foundation, one test pack per engine (crates/core, crates/detect, crates/html tests) proving each hook fires, that the built-in findings are unchanged, and that the waivers and the disabled-rules list cover pack rules, plus the wasm export shapes. Workspace tests 346 to 361, oracle 795/0 unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist under the open design Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * bundle: the page JS and the bundler become a library crate downstream packs can reuse The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * The immediate tier moves to the registry, and reaches wasm The design hook's immediate-tier list is the set of rule ids worth fixing at the edit site, and a downstream reviewer wants the same set to decide how loudly a finding is reported. `impeccable-hook` is native-only, so the list moves to `impeccable_core::registry` (the hook re-exports it) and the `detect` feature gains `immediate_tier_rules_json()`. The export is behind `detect`, which the in-page bundle does not build, so the tracked browser asset is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: Pristine tracks the engine by revision pin, not npm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist is maintainer-side, not part of the tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix flat type hierarchy false positives (#702) Upstream sha 84728e9ce43a3dba2a453b20130bbf836190d77c. The rule now reads rendered semantic roles and the dominant size per role instead of the raw set of font sizes on the page, and it fires only when every adjacent role step is under 1.25x. - crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES / MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and checkFlatTypeHierarchySamples, the shared half of checks.mjs. - crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over the Dom trait, with the overlay skip selector checkTypography passes. - crates/html page.rs gets the same walk over StaticDocument. - crates/detect drops the source-only analyzer: flat-type-hierarchy leaves REGEX_ANALYZERS, the text-content analyzers shift to index 1, and analyzer_rule_id loses its first row. - crates/html cascade defaults gain contentVisibility, and crates/foundation registry carries the reworded description. Goldens re-recorded (the binary now matches origin/main's JS engine on every one of these fixtures, verified by scanning the shared corpus with both): glow, icon-tile-stack, layout, modern-color-borders, motion, named-color-borders, numbered-section-markers, oklch-neon-text, typography-should-flag, json and text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix detector URL scans and advisory handling (#709) Upstream sha fa44839f7289fced3f51946684656a28775638cc. Advisory handling. `severity` becomes the canonical registry field: the `advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on `severity == "advisory"`, and `derive_advisory_flag` stamps the finding's `advisory: true` from the effective severity, so a per-finding promotion or demotion carries the flag. The html and browser engines call it after their severity override; the detect CLI and the hook accept either spelling; the driver's serializer and the wasm registry exports derive it the same way. em-dash-overuse moves from `advisory: true` to `severity: "advisory"`. URL scans. `expand_joined_url_targets` splits an argv value that is entirely whitespace-separated URLs and leaves paths with spaces alone. The browser driver reads the readable linked-stylesheet corpus into the HTML pattern corpora and resolves a finding's selector with `selector_nodes_for_live_dom` / `pseudo_element_host_selector`, so an unresolvable selector drops the finding instead of keeping it page-level. The CSSOM walk itself is page JS: `browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping rules flattened, container-query probes, effective keyframes) and puts it in the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page route, and the Dom trait carries `linked_stylesheet_text`. Also `enclosing_css_selector` blanks comments before hunting the previous declaration delimiter, and `check_typography` reports the uniquely most-used family instead of every family over a 15% share. Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns` is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree over the shared corpus (432 findings). The two changed lines in tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were re-recorded by running origin/main's `checkHtmlPatterns` over the frozen args; only the comment-polluted selector changed. Goldens re-recorded for the advisory partition (config-*, fixture gemini/gpt-tells, numbered-section-labels, scoped-ignore, shape-assembled-illustration, color, em-dash-entities) and the help text, each cross-checked against the JS on origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707) Upstream sha 32b270f4e8ec0af40ef85508c224f0f49096bd7d. `find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex` crate has no lookahead, so the maximal digit run plus the word boundary is matched as before and the byte after it is tested for `/`. The text engine gains the JS-source scanner (`scan_js`) and the scope helpers on top of it: `containing_markup_tag` keeps a gray text class from pairing with a background in a sibling tag on the same line, and `find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b` class expression into its arms, recursing into nested ternaries, ignoring `?.` and `??`, and keeping a common prefix and post-ternary suffix in every arm. `MatchCtx` now carries the match offset the scope lookup needs. Verified against origin/main's JS: all eleven cases from the upstream test file plus a nested / nullish / suffix set produce byte-identical findings on both engines; they are pinned as Rust unit tests in `regex_matchers` and `checks::rules`. The shared fixture corpus stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: resolve unique --target names in monorepos (#706) Upstream sha 8b326fc81e026fffbcdcecd94ce34af956ebea79. `resolve_target_path` / `find_unique_bare_target` in `crates/context`: a `--target` that does not exist and reduces to a single path segment under cwd resolves to the one workspace candidate with that name, so `--target a` selects `apps/a`. A caller that already absolutized the name against cwd (live and the other helpers do) takes the same route. Ambiguous or unknown names still report the miss. The context CLI resolves the target once and hands the resolved path to `load_context`, replacing `path_exists_for_target`. Oracle: four new `context-monorepo-target-bare-*` cases (bare name, absolutized bare name, unknown name, bare name from a child cwd). `context-monorepo-target-b-inherits` was re-recorded: resolving the target before `load_context` changes its `surfaceBriefReason` from `not-found` to `invalid-target`, which is what origin/main's `context.mjs` prints for the same run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Next.js 16 CSP and parent hook discovery (#710) Upstream sha 672ca29642b513bc3365afb0e309fff3d6dfa752. CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook beside `middleware.*`, but only where it sits at a project root or its `src/` directory: the scan root itself, or a nested directory carrying a Next project marker (a `next.config.*`, an `app` / `pages` dir, or a `next` dependency). A same-named helper elsewhere in the tree is not the framework hook. Context. `find_git_boundary_root` gives `resolve_project` a git-boundary notion: an explicit target inside its own repository resolves against that repository, and an external target resolves against its own root, so caller context never leaks across the boundary. `hook_manifest_search_roots` replaces the cwd/projectRoot/repoRoot triple with a walk up from the project root that stops at the first git boundary, and each root's own hook lifecycle config is honored before its manifest counts as coverage. Verified against origin/main's JS: nine `detect-csp` placements and five hook-discovery scenarios (enclosing harness root, that root disabled, sibling target, nested git target, markerless nested git target) produce identical output. Oracle: five `csp-proxy-*` cases and five `context-hook-*` / `context-markerless-nested-git-target` cases. Four route-target goldens were re-recorded because #710 resolves a `/`-prefixed target outside the workspace; each was cross-checked against origin/main, and `surface-brief-write-route` has a DELTAS entry for the one wording difference (an unwritable filesystem root). `tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from the merge importing the deleted `detectCsp`; it now drives `detect-csp` through the binary like the rest of that file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: fail URL scans when the browser is unavailable (#711) Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: OpenCode slash command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78. OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a pinned skill never reaches its slash menu. `pin` now writes `commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and skips `.opencode` in the SKILL.md loop so no unreachable `.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded, and reaches both scopes even when the skill itself is gone. `find_opencode_commands_dirs` covers the project-local dir when the project has the skill and the user config dir when Impeccable is installed globally, resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`). The build-tooling half of the upstream change (transformers, the OpenCode command the build generates, `root-commands-sync`) came in with the merge and needed no port. Verified against origin/main's pin.mjs across seven scenarios (no harness, project scope, user scope, a foreign command file, pin then unpin, unpin over a foreign file, unpin with nothing pinned): identical stdout, identical file sets, identical file contents apart from the one deliberate difference. Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body naming the launcher rather than `node .../context.mjs`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Codex skill version metadata (#703) Upstream sha 482368511ace07982a7cd3a23dd60cf62d6f68c8. Codex's validator rejects unknown top-level keys, so the Codex and `.agents` skills now carry `version` under the spec-defined `metadata:` map. Both version readers learn the same parser: `parse_skill_frontmatter_version` in `crates/context` (the boot update check) and `extract_version` in `crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy top-level one still reads, only the map's own indent level counts, tabs count as two spaces, and a comment line is skipped. The build-tooling half (`versionInMetadata` on the two providers, the YAML emitter's nested-object branch) came in with the merge. Fourteen frontmatter shapes were recorded from origin/main's `parseSkillFrontmatterVersion` and pinned as unit tests in both crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix skill subcommand help handling (#708) Upstream sha a26419917716b16623cc830429f3cc1a4f7cd630. `install`, `link`, `update` and `check` render static help before entering any operational path, through both the top-level verb and the legacy `skills` namespace, for `--help` and `-h` alike. Verified against origin/main's `cli/bin/cli.js`: all six spellings produce identical text and exit codes. Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the help paths are pinned there; every other installer path writes into harness directories or reaches the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle: goldens for the three fixtures the merge added `tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: the installer half of the OpenCode command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78, the part of it that lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`. `copy_provider_commands` mirrors `copy_provider_skills` for a provider's compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`, user scope writes the config dir OpenCode actually scans (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`), and a pre-#406 global install at `~/.opencode/commands/` loses exactly the files just written while siblings, symlinked dirs and home-rooted git repos are left alone. It runs on install, on the reinstall refresh, on update, and on link, which is the only path that can deliver the bridge to a linked install. `is_up_to_date` now compares the bundle's command files too, so an install whose skills match but whose bridge is missing or drifted refreshes instead of reporting success while the slash command stays absent. Only bundle-shipped files are compared, so a pinned shortcut never affects freshness. `tests/copy-provider-commands.test.js` arrived with the merge importing the deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to `crates/skills/tests/provider_commands_tests.rs` (project scope, the three user-scope dir resolutions, the legacy migration and its two guards, a provider with no commands dir, and the four `isUpToDate` command-awareness cases), and the file is removed and deregistered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CI: the first full run on the branch, three fixes - The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context tests: the verbatim-prefix strip spells the prefix once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: derive the snapshot identity from the verb's own resolver Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: JSON-quote the snapshot identity, as the verb does Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * detect test: import resolution against platform-form paths Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills tests pass on Windows The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the kernel takes a verbatim path literally, so every `/`-joined path built under them was an invalid filename. Strip it the way Node's `realpathSync` does. The manifest, artifact and sibling-binary expectations hard-coded POSIX separators for paths the product joins with the host's semantics; derive them from `jsp::join` instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests pass on Windows Same verbatim-prefix strip on the test temp roots, plus expectations derived from the helpers the product uses: cache keys and scan targets from `jsp::join`, the config path in an admin message from the same relative form `path.relative` renders, and the footer hints from `quote_command_arg`, which deliberately switches to the double-quoted Windows form (#476 / #533). The env lock no longer poisons the sibling tests when one of them fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: html oracle goldens compare on Windows The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then render the remainder with `/` so a Windows checkout's backslashes are not read as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: widen the live read-deadline test's margin Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system timer while the crate's tests run in parallel, so the later request takes its turn later there. The bound stays far under the 60s read timeout a deadline-less read would hold the ticket for, so the test still distinguishes the fix from the regression. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests derive the rest of the host path forms The test temp helper's `write` returned a `PathBuf::join` result, which keeps the `/` inside the relative part and so does not match what the hook resolves a relative target to on Windows. Three more admin messages and the cache-root slug pinned the POSIX spelling of paths the product renders with the host's semantics (`path.resolve` also prefixes the current drive there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills test fixtures name USERPROFILE, and the win32 quoted form `os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only HOME sent the global installs into the runner's real profile. The Windows hook command carries the JSON-quoted path, so a host path's backslashes arrive escaped; derive the expectation instead of pinning the POSIX spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the oracle fixtures out with LF A finding's snippet carries the scanned file's own bytes, and the goldens were recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the grok global-install manifests as JSON The Windows hook command carries the JSON-quoted launcher path, so the path's backslashes are escaped once inside the command and again by the manifest file itself. Read the manifest as JSON and look for either quoting form instead of counting escaping layers in a raw substring match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * npm shim: refuse a download with no verifiable sidecar The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle fixture: declare the vite plugin the web workspace imports `live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Vectors: drop the 12,208 byte-identical repeat lines The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Fix: restore the live overlay's disabledValues waivers in the engine The JS engine applied value-level ignore waivers at the tail of collectBrowserFindings: `_disabledValues` read the entries the live overlay resolved for the page (skill/scripts/live-browser-ignores.js sends them as config.disabledValues), and filtered the assembled findings by the value each one reported, with design-system-color compared by color value rather than by spelling so a hex waiver suppressed a finding the browser reported as rgb(...). The Rust port dropped that stage: `disabledValues` appeared nowhere in the workspace or in browser-bundle, so a project entry like [detector] ignoreValues = [{ rule = "overused-font", value = "geist mono" }] stopped reaching the overlay. The rules the CLI and the edit hook waive kept drawing markers and counting toward the badge. Restore it end to end: * BrowserConfig gains `disabled_values`, parsed leniently so a hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped rather than failing the whole config, the way the JS filter did. * The driver applies the waivers after every pass, so a rule pack's findings are covered the same way the built-in ones are, honoring the entries only in extension mode exactly as the JS read them. The normalizer, the value extractor (including the rule that bounce-easing without a direct ignoreValue offers no value) and the hex/rgb color key are ported alongside it. * collectConfigJson in the in-page bundle and configJson in the offscreen bundle forward the field. The extension never sends it, so its behavior is unchanged. Coverage: two driver unit tests (suppression by font value, by hex waiver across the rgb spelling, and the extension-mode gate; plus the config parse and the normalizers), a skipScan test that pins the empty shape for every stage the core produces, and crates/wasm/tools/disabled-values-check.mjs, a browser-backed check ported from the retired tests/detect-antipatterns-browser.test.mjs case that the swap left without a replacement. Against the previous bundle it fails on exactly the three waiver assertions and passes the skipScan one, which is the shape of the regression. Two related review findings were checked and are not defects. skipScan is gated on extension mode in both the driver and the bundle, which is what the JS did (index.mjs#skipScanActive), and the live overlay runs in extension mode: live-browser.js sets `s.dataset.impeccableExtension` on the injected /detect.js tag, and the overlay's whole detect toggle travels over the postMessage loop that 50-scan.js installs only under EXTENSION_MODE. The visual contrast stage is not leaking either: collectBrowserFindingsAsync and scan() both consult skipScanActive(), and the offscreen path skips its visual pass on config.skipScan. The tracked live asset is regenerated (cargo xtask bundle). The oracle replays with zero unreviewed differences: the new field defaults empty and the filter is inert without it, and no CLI path sets extension mode. AI-assisted change: implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Shim test: run from a staged copy and prove the download happened The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the loader now hands off when the resume is the arrival The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live server: stop ends the process, SSE skips the mutation lane Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the shader teardown can no longer race its own construction The new cycling assertion caught a real defect on CI: vite8-react-insert reached CYCLING with #impeccable-live-shader still painted over the page. showShaderOverlay is async. It appends its canvas synchronously, then awaits createImageBitmap and finishes the GL setup before it publishes shaderState. hideShaderOverlay returned early on a null shaderState, so a teardown that landed inside that window did nothing, and the construction then published itself over a session that had already left GENERATING, with no teardown left to run. The scroll tick kept repositioning it, which is why the CI page.html shows the canvas sized from the capture rect but styled to the cycling anchor. Every teardown now bumps a shader epoch before it does anything else, and a construction pins the epoch it owns and abandons its canvas (releasing the GL context) at every point past an await and before any publish, including both bitmap-fallback publishes. A teardown also drops a shader node that no shaderState owns, so an already-orphaned canvas cannot survive one. Reproduced by widening the append-to-publish window: with a 400ms delay after uiAppend, vite8-react-insert failed with the CI error and the probe showed the teardown arriving at CYCLING with shaderState still null. The same run passes with this change, as does a 1500ms window on insert and plain. Locally that window is about 4ms, which is why it only showed on a slower runner. The four remaining setLiveState('CYCLING') sites that did not lower the loader now do: the SSE done handler (the one route that can reach CYCLING from GENERATING), the Svelte republish remount, and the two accept failure recoveries. The e2e assertion already waits up to 5s for the shader to clear, so it was never racing a legitimate teardown; it is left as it is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: every active-session wrapper lookup goes through the resolver Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: a discard releases every wrapper it hid Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Release: publish the npm platform packages in one command bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: pin checkout, upload-artifact and download-artifact at v7 The v4 pins target Node 20, which the runner now deprecates and forces onto Node 24 with a warning on every step. The rest of the workflows already use v7. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: make the temp-dir helpers unique under a coarse clock Windows' system clock is coarse enough that two parallel tests could get the same pid-plus-nanoseconds directory name and then remove each other's files (rust-windows: close_verb_round_trip_and_ownership, NotFound). A per-process counter is appended to the name. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: declare the temp-dir counter in the hook cache-root tests The previous commit referenced TMP_SEQ there without defining it. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-09-04 10:42:45 -07:00
"test:oracle": "node scripts/run-tests.mjs oracle",
"test:detector": "node scripts/run-tests.mjs detector",
"test:framework": "node scripts/run-tests.mjs framework",
"test:live": "node scripts/run-tests.mjs live",
Tests: stop the harness leaking live-server processes (#718) * Tests: stop the harness leaking live-server processes Nothing owned a live server past the exit paths JavaScript can observe. The live unit tests spawn the server as a direct child and stop it with an HTTP /stop plus proc.kill() inside an after() hook; the e2e session and the target-context tests boot it through `live-server --background` / live.mjs, which spawns a detached, unref'd daemon that only the `stop` verb ever ends. A POSIX child does not die with its parent, and a detached daemon is orphaned to pid 1 from birth, so any exit that skipped teardown (a node:test timeout, a SIGKILL of the runner, a Ctrl-C, an assertion that threw before the hook) left the server listening on a fixed live-suite port for good. scripts/run-tests.mjs did not compensate: it used blocking spawnSync, so no signal handler could run; it left suite commands in its own process group with nothing that could kill that group; and it never checked afterwards whether anything survived. Days of local runs accumulated 197 orphans on one machine, the oldest four days old, until `bun run test:live` could not claim its ports. The fix is structural rather than a cleanup sweep bolted on the end, and it is deliberately implementation-agnostic so it holds for the Node scripts here and for the Rust `impeccable live-server` on rust-swap: - tests/lib/live-servers.mjs. armLiveServerReaper(), called once at module scope by every test file that starts a server, stamps the process env with a unique marker, installs exit and signal handlers, and spawns a detached reaper holding a pipe to the process. SIGKILL the process and the pipe closes, the reaper wakes on EOF and kills the servers carrying that marker. That is the one case no in-process cleanup can reach. trackServerChild() also registers direct children (live servers and fixture dev servers) so the ordinary exits are a cheap kill by handle. - scripts/lib/live-server-processes.mjs. The scan and kill primitives, shared by the reaper and the runner. Processes are matched by the environment marker the harness exported, never by name or port, so a sweep can only ever reach a server this repo's tests started. - scripts/run-tests.mjs. Each suite command now runs as its own process-group leader with SIGINT/SIGTERM/SIGHUP forwarded to the group, and after every suite the runner checks for live servers carrying that suite's run id. A survivor is killed and fails the run, so the next leak surfaces in the run that caused it instead of on a laptop days later. IMPECCABLE_SKIP_LEAK_CHECK=1 bypasses it. `bun run test:cleanup` sweeps leftovers from earlier runs. - tests/live-server-leak.test.mjs pins the guarantee: it boots a real server under a process it then SIGKILLs, and fails if the server outlives it. With IMPECCABLE_NO_TEST_REAPER=1 the test fails, which is what makes it a regression test rather than a tautology. Verified: bun run test:live green with zero survivors; scoped live-e2e (vite8-react-plain) matches pristine main test for test; the SIGKILL repro goes from 2 orphans to 0; SIGINT and SIGKILL of the runner itself both leave nothing behind; bun run build green. Fixes #717 AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Review fixes: scope the sweep to whole env entries only Five review findings on #718, all in the matching layer that decides which processes a sweep may touch. The repository-path fallback is gone (Greptile P1). `bun run test:cleanup` passed REPO_ROOT to findLiveServers, which then also matched any live-server command line under the checkout, marker or not. A developer running `impeccable live` in this repo has exactly that command line, so the cleanup could have killed their own session. The PR promised matching on the exported environment marker and nothing else; now it does. The cost is that a server from a run predating the marker is no longer found and has to be killed by hand, which is the right trade. Environment entries are compared whole on macOS and BSD (Greptile P1). `ps -E` flattens the environment into the command column, and that line was searched with a plain substring test, so IMPECCABLE_TEST_REPO=/work/impeccable also matched /work/impeccable-copy and one checkout's cleanup could reach a neighbouring checkout's servers. envLineHasEntry() now requires the marker to start an entry (line start or whitespace) and to end one (line end, or whitespace followed by the next KEY=), which is the same whole-entry comparison the Linux /proc branch already did. Six unit tests cover it, including the adjacent-path negative case, and a live probe against real `ps -E` output confirms an exact repo matches while /work/impeccable-copy and a run-id prefix do not. The SIGKILL regression test now skips on win32 with a stated reason (Copilot). The reaper is a POSIX mechanism and armLiveServerReaper() does not arm it there, so the test asserted a guarantee Windows does not make yet. Signal exits use the shell convention 128 + signum in both the runner and the test helper (Copilot, two threads). SIGHUP returned 143; it is 129. Read from os.constants.signals rather than a hand-written table. Verified: leak test 7/7 (2 guard, 5 matcher); bun run test:live 895 tests, 0 fail, 0 survivors; scoped live-e2e (vite8-react-plain) 3 pass / 1 fail, matching pristine main; SIGKILL repro 3 servers up, 0 after; bun run build green. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Review fix: make marker values opaque so the matcher has no ambiguous case Greptile's follow-up P1 on the parser was right, and the parser was the wrong place to answer it. envLineHasEntry ended an entry at "whitespace followed by the next KEY=", so a checkout path that extended another one with whitespace plus a KEY=-shaped token still defeated it, which is exactly the ambiguity the docblock admitted to. A format that cannot be parsed unambiguously should not be handed ambiguous input. So the fix is at the source: no marker value is a path any more. IMPECCABLE_TEST_REPO now carries repoMarker(), the first 16 hex characters of the sha256 of the checkout's real path, and the runner and the cleanup command both compute it the same way from REPO_ROOT. Two checkouts whose paths share a prefix get unrelated hashes, so a substring cannot arise in the first place, and every spelling of one checkout (trailing slash, `.` segment, symlink, /private prefix) resolves to one marker. The run id is now repoMarker plus 8 random bytes of hex, and the process id p<pid> plus the same, both from a whitespace-free alphabet. With every value fixed-alphabet, envLineHasEntry needs only "starts an entry and ends at whitespace or line end". The KEY= lookahead is gone and so is the documented unresolvable case. assertMarkerValue keeps the invariant honest: it refuses any value outside [A-Za-z0-9_-] with a message that says to hash it, so a future caller that passes a path gets a loud error instead of a silent mismatch. The readable path is still available for a human reading `ps -E` output, exported separately as IMPECCABLE_TEST_REPO_PATH, which nothing matches on and the docblock says so. Matcher tests: the space-in-value case is gone, since that value can no longer exist. Added a strict-prefix case (a longer hash-shaped value starting with the marker), an adjacent-checkout case asserting the two hashes do not even share a prefix, a symlink/trailing-slash case against real directories, an alphabet check on all three generators, and one asserting assertMarkerValue throws. Verified: leak test 10/10; bun run test:live 898 tests, 0 fail, 0 survivors; scoped live-e2e (vite8-react-plain) 3 pass / 1 fail, matching pristine main; SIGKILL repro 1 server up, 0 after; bun run build green. A probe against real `ps -E` output with a hashed marker: this checkout 1 match, its trailing-slash spelling 1, an adjacent checkout 0, exact run id 1, a run-id prefix 0. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Review fixes: async group shutdown, and a Windows-safe symlink test Two Cursor Bugbot findings, both real. killCurrentGroup busy-waited on alive(child.pid) after sending SIGTERM, which could never work. A dead child stays a zombie until its parent reaps it, the parent here is the runner, and the runner reaps through libuv when the event loop runs. The spin blocked the very loop that would have done the reaping and then read the unreaped zombie as alive, so every SIGINT, SIGTERM and SIGHUP burned the full 2s grace and ended in a needless SIGKILL. There is no waitpid from JavaScript that sees through this, so the wait is now asynchronous and keyed on the child's own exit event. The logic moved to scripts/lib/process-group.mjs: trackChildExit exposes the exit as a flag and a promise, stopGroup races that promise against the grace period and escalates to SIGKILL only if it loses, and killGroupSync stays synchronous for process.on('exit'), where nothing can be awaited, so it sends SIGTERM then SIGKILL without pretending to wait. A second Ctrl-C now skips the grace period entirely rather than queueing behind it. Measured on a real SIGINT to a running live suite: 2027ms before, 34ms after. tests/process-group.test.mjs pins both halves, including the escalation path against a child that traps SIGTERM, which is not otherwise reachable from a registered suite. The repoMarker symlink test called symlinkSync with no type, which throws EPERM on Windows without Developer Mode. It now passes 'junction' there and 'dir' elsewhere, the same shape tests/concept-seed.test.mjs uses, and the trailing-slash and dot-segment cases split into their own test so they keep running on every platform regardless. Merged origin/main (through #716) to re-level the branch. Verified: leak and process-group tests 16/16; bun run test:live 900 tests, 0 fail, 0 survivors; scoped live-e2e (vite8-react-plain) now 4/4, with the orphaned-session test that #716 fixed passing in 7.2s; bun run build green. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Review fix: a second Ctrl-C must reach the group the first one is stopping Cursor Bugbot caught a bug I introduced with the async shutdown, and it is the same class of leak this PR exists to close. The signal handler cleared currentChild before awaiting stopGroup, so a second Ctrl-C read a null handle: killGroupSync did nothing, process.exit walked away from the SIGKILL escalation still in flight, and because the suite is spawned detached it kept running after the runner was gone. Impatience with a stuck suite produced exactly the orphan the change is supposed to prevent. The shutdown state machine moved into scripts/lib/process-group.mjs as createGroupShutdown, which holds the group in `stopping` for as long as it is being ended rather than dropping the only reference to it. A second signal kills that handle and leaves; process.on('exit') looks at `current` or `stopping`, so the last-resort path reaches a group mid-shutdown too. The runner keeps no shutdown state of its own now, which is what made the bug possible to write in the first place. The extraction is what makes it testable: `exit` is injectable, so tests/process-group.test.mjs can drive two signals at a stubborn child that traps SIGTERM and assert the group dies in under 2s against a 30s grace. Point that test at the old logic (killGroupSync on the cleared reference) and it hangs out the full grace and fails, which is the check that it pins something real. Five cases in all, including the exit-handler path and the no-child case. Verified: process-group 10/10, live-server-leak 11/11; real double SIGINT to a running live suite exits in 24ms with zero group members and zero servers left; bun run test:live 900 tests, 0 fail, 0 survivors; scoped live-e2e (vite8-react-plain) 4/4; bun run build green. The core suite wedged twice locally in tests/build-phase.test.mjs, the pre-existing unbounded-spawnSync hang noted in the PR description that rust-swap's 47f18713 fixes. Unrelated to this change: CI is green on both Node versions, and process-group.test.mjs passes inside that batch. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 22:21:30 -04:00
"test:cleanup": "node scripts/run-tests.mjs --cleanup",
"test:cli-remote-e2e": "node scripts/run-tests.mjs cli-remote-e2e",
Guard the plugin loader contract that PR #494 exposed (#499) * test: guard the plugin loader contract that PR #494 exposed The agents manifest key shipped for months and silently loaded zero of the four subagents; no validator looked at the generated plugin manifest's shape and claude plugin validate never checks it. Three layers now do: - scripts/lib/validate-plugin-manifest.js pins the verified loader contract (KNOWN_LOADER_KEYS allowlist, no agents key, trailing-slash skills path from issue #86, every skill/agents/*.md shipped in plugin/agents/), unit-tested in tests/validate-plugin-manifest.test.js including a check of the real committed subtree. - The same check gates bun run build next to the version-drift guard. - tests/plugin-e2e.test.mjs installs the committed ./plugin subtree into a real Claude Code (sandboxed via CLAUDE_CONFIG_DIR in a temp dir) and asserts the component inventory: skill parses, all agents visible, hooks discovered. In the default suite; runs in about a second and skips cleanly when the claude CLI is absent, so CI is unaffected. All three failed against the pre-#494 tree for the shipped reason (Agents 0 of 4) and pass against current main. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: address PR review bot findings - Copilot: guard collectPluginManifestFindings against valid JSON that is not an object (null, string, number, array) so a broken manifest is a finding instead of a build crash; unit test added - Copilot: update the plugin-e2e header comment, the suite is in the default lineup rather than opt-in AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: harden plugin E2E sandbox isolation Bugbot: create the sandbox CLAUDE_CONFIG_DIR up front and redirect HOME and USERPROFILE into the temp workDir too, so a CLI code path that derives config or cache locations from the home directory instead of CLAUDE_CONFIG_DIR still cannot touch the developer's real Claude config when the default suite runs. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: agent parity check mirrors the build's emit rules Bugbot: the shipped filename is `${claude-name || name}.md` and a providers: list may exclude claude-code, so comparing raw source basenames could fail the build on a renamed or provider-scoped agent with a build:release hint that cannot fix it. The validator now derives expected filenames the same way the transformer factory does (shared parseFrontmatter, same providers gate) with unit coverage for renames, name overrides, and provider-scoped agents. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: run the plugin E2E through a shell on Windows Bugbot: the claude CLI is a .cmd shim on Windows and Node refuses to spawn those via execFile without a shell, so the availability probe always failed and the suite silently skipped there. Windows now invokes through a shell with every argument double-quoted (temp paths routinely contain spaces); the POSIX path is unchanged. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-03 13:13:37 -07:00
"test:plugin-e2e": "node scripts/run-tests.mjs plugin-e2e",
"test:live-e2e": "node scripts/run-tests.mjs live-e2e",
"test:live-e2e-accept-cleanup": "node scripts/run-tests.mjs live-e2e-accept-cleanup",
"test:new-work-e2e": "node scripts/run-tests.mjs new-work-e2e",
"test:live-e2e-agent": "node scripts/run-tests.mjs live-e2e-agent",
"test:skill-behavior": "node scripts/run-tests.mjs skill-behavior",
"test:skill-workflow": "node scripts/run-tests.mjs skill-workflow",
"test:live-svelte-adapter-deepseek": "node scripts/run-tests.mjs live-svelte-adapter-deepseek",
Add automatic design hook install and exceptions (#170) * docs: add PRD for design detector hook integration Plans a PostToolUse hook for Claude Code and Codex that runs the existing design detector after every relevant file write and feeds findings back to the agent as advisory system-reminder context. No implementation in this commit; covers UX, technical design, build pipeline changes, distribution, coverage tradeoffs, and rollout. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: revise hook PRD with best-practices review Folds in the P0/P1/P2 findings from an online best-practices critique against the official Claude Code and Codex hook references plus 10+ 2026 community guides and similar prior-art tools (claw-hooks, claude-code-hooks-mastery). Key changes: - Exec form everywhere (Codex snippet was shell form), with Windows rationale. - Default timeout dropped from 10s to 5s. - Re-entrancy guard (CLAUDE_HOOK_DEPTH) and per-file edit counter. - Session-scoped finding dedup promoted from open question to v1. - Per-language inline-ignore syntax map (HTML/JSX/CSS/JS). - Hard-skip rules for sensitive paths and generated/lock files. - Honest framing about Claude Code lacking per-plugin hook disable. - Honest framing about Bash-written files being invisible in v1. - Codex Windows-not-supported call-out, feature flag note, trust ceremony detail. - Optional NDJSON audit log via IMPECCABLE_HOOK_LOG. - Findings cap lowered 8 → 5 with attention-budget rationale. - Versioned envelope ([impeccable@1]) on rendered template. - Expanded test plan, decision log, and stdin payload appendix. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(hooks): ship the design detector hook for Claude Code and Codex Implements docs/hooks-prd.md: a PostToolUse hook that runs the impeccable design detector after every Edit/Write/MultiEdit on a UI file and pushes findings into the agent's next-turn context as a short system reminder. Silent on clean files. Never blocks an edit. Why this matters: today, design slop (side-tab borders, gradient text, purple/cyan palettes, bounce easing, etc.) only gets caught when a human notices or someone explicitly runs /impeccable audit. The hook closes the loop at the moment slop is written. What ships in v1 - skill/scripts/hook.mjs: PostToolUse entry. Reads stdin, runs the detector in-process (no `npx impeccable` cold start), emits hookSpecificOutput.additionalContext when fresh findings exist. - skill/scripts/hook-lib.mjs: extracted helpers (config, cache, filter, render, audit log, runHook orchestrator). 100% unit-testable. - skill/scripts/hook-session-start.mjs: SessionStart greeting, gated by a project-scannable probe + 30-day throttle. - skill/scripts/hook-admin.mjs: backs /impeccable hooks on/off/status/ignore-rule/ignore-file/reset. Hardening built in - Re-entrancy guard (IMPECCABLE_HOOK_DEPTH) so the hook can never recursively spawn itself. - Hard-skip regexes for sensitive paths (.env, .pem, id_rsa, secrets, credentials, .git) and generated/lock/build output. These fire before the file is even read; cannot be turned off via config. - Path-traversal check on the inbound file_path. - Session-scoped dedup keyed by (session, file, rule, line) so the same finding never lands in context twice. Prevents the ~12.5K wasted tokens per chatty session called out in the PRD. - Per-(session, file) edit counter with a one-shot suppression notice on the 7th edit, silent after. - Fail-open contract: every error path returns exit 0 with no stdout. Optional NDJSON audit log via IMPECCABLE_HOOK_LOG. Three kill switches (precedence high to low): 1. IMPECCABLE_HOOK_DISABLED env var (1/true/yes/on, case-insensitive) 2. .impeccable/hook.json `enabled: false` 3. /impeccable hooks off slash command (writes the JSON) Inline ignores are language-aware. `// impeccable: ignore <rule>` for JS/TS, `<!-- impeccable: ignore <rule> -->` for HTML/Vue/Svelte/Astro, `{/* impeccable: ignore <rule> */}` for JSX/TSX, `/* impeccable: ignore <rule> */` for CSS. `*` matches any rule. Directive applies to the next non-blank line. Same shape as ESLint, Stylelint, Biome. Build pipeline - scripts/lib/transformers/hooks.js: per-provider hooks.json builders, plus the slim .codex-plugin/plugin.json manifest. - providers.js: emitHooks: 'claude' for claude-code, emitHooks: 'codex' for codex and agents. Codex also emits emitCodexPlugin. - factory.js: emits hooks/hooks.json next to the skills tree. - build.js: syncs hooks/ into harness roots and into the slim plugin/ subtree; writes .codex-plugin/plugin.json. Build is idempotent (verified: 98 staged files unchanged across two runs). Claude Code wiring uses exec form (command + args) and the ${CLAUDE_PLUGIN_ROOT} placeholder. Matcher: Edit|Write|MultiEdit. `if:` glob filters to UI extensions before spawning Node. PostToolUse timeout 5s, SessionStart timeout 3s. Codex wiring uses ${PLUGIN_ROOT} (Codex's native placeholder), matcher Edit|Write|apply_patch, no `if:` analog (the script does the extension filter). macOS and Linux only; hooks are disabled on Windows in current Codex builds. The trust ceremony and feature flag are documented in README.md. Routing - /impeccable hooks lives outside the 23-command router table on purpose: it is plumbing, not a design skill. The hidden routing slot is added to SKILL.md alongside pin/unpin so the LLM knows to dispatch it. The 23-command count and all stale-count validators remain happy. Tests - tests/hook.test.mjs: 38 unit tests covering env parsing, config load + defaults + malformed, cache round-trip + GC, ignoreRules/minSeverity/inline ignores (all four languages), globbing with **/*/{a,b}, render template with cap + clamp + 0-line prefix drop, audit log NDJSON, payload event-name parameterization, re-entrancy, kill switches, sensitive-path + generated-path + traversal skips, allowlist filter, config ignoreFiles, edit counter cycle including the 7th-edit notice, MultiEdit and apply_patch payload shapes, detector throw swallow, malformed stdin, missing file race. - tests/hook-build.test.mjs: 18 integration tests covering hook manifest shape (matcher, timeouts, exec form, if: glob, placeholders), Codex differences (${PLUGIN_ROOT}, no if:, no SessionStart), Codex plugin manifest (no inline hooks field to avoid the duplicate-file error), routing across the hooksJsonFor table, and presence of all three committed artifacts plus the bundled detector the runtime relative-import path depends on. Full suite: 175 bun tests + 186 node tests, all green. Docs - README.md: new "Design hook" section explaining default behavior, per-project / global / inline disable paths, the JSON schema knobs, the audit log debug flag, and the slop / a11y coverage split. - HARNESSES.md: flips the `hooks` row for Codex from No -> Yes (Claude was already Yes), adds a per-harness hook-surface table with the manifest location and matcher each provider uses. Open questions from the PRD intentionally deferred to v2: Bash-write blind spot, effort-aware suppression, Stop-hook session summary, per-rule severity, async hook mode. None block v1. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Codex hook scanning: apply_patch paths and co-located stylesheets Parse file targets from Codex apply_patch command bodies, co-scan imported and sibling CSS when UI components are edited, drop the git-sweep PostToolUse group, and align Codex SessionStart manifest and trust docs with the official hooks spec. Co-authored-by: Cursor <cursoragent@cursor.com> * Gitignore hook session cache and drop local test HTML Hook dedup/throttle state in .impeccable/hook.cache.json is per-project runtime data like other .impeccable/ sidecars. Remove an untracked bad-nested-flexbox scratch page from site/public/. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Claude Code hook: drop Edit-only if filter so Write/MultiEdit fire Claude's if permission rule binds to one tool name, so Edit(*.{…}) never spawned the hook on Write or MultiEdit despite the matcher listing them. Extension filtering now lives in hook-lib on both Claude and Codex. Co-authored-by: Cursor <cursoragent@cursor.com> * Surface Cursor design findings via stop-hook followup Replace dropped postToolUse additional_context with afterFileEdit recording and a one-shot stop followup_message so anti-pattern nudges reach the agent. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix design hook packaging and scans * Fix Cursor hook pending bucket fallback * Fix Sass hook scan coverage * Fix Cursor hook review findings * Fix session start dead hook normalization * Fix hook config and relative scan paths * Remove SessionStart design hook * Remove redundant afterFileEdit normalization * Fix Cursor suppression and module style scans * Fix sensitive path hook filter * Fix disabled Cursor stop hook emission * Refresh hook harness artifacts * Fix Cursor hook manifest install * Add hook ignore-value support * Ignore hook runtime files locally * Fix Codex plugin hook packaging * fix: address PR review bot findings Block numeric hook depth counters from re-entering. Avoid following stylesheet imports from traversal-looking hook targets. * fix: gate ignore-value suggestions by supported rules Only render exact ignore-value commands when the same finding can be suppressed by ignoreValues. * Package Codex plugin as hook-only * Remove Codex plugin packaging * Recover hook install probe plumbing * Remove Codex hook packaging follow-up doc * Remove extra hook docs and skill wording changes * Install real design hooks via skills CLI * Add provider hook smoke runner * Fix Cursor hook delivery with preToolUse gate * Simplify Cursor hook install to preToolUse * Clarify confirmed hook exceptions * Persist hook ignores in shared config * Guard font hook exceptions * Fix hook install after main rebase * Fix hook scan target handling * fix: address hook review findings * Address hook review feedback * Stabilize DeepSeek insert live fixture * Fix Cursor hook Python shell write bypass --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 13:19:19 +09:00
"smoke:hooks": "node scripts/smoke-provider-hooks.mjs",
"audit": "bun audit --audit-level=moderate",
"prepack": "cp README.md README.repo.md && cp README.npm.md README.md",
"postpack": "cp README.repo.md README.md && rm README.repo.md",
"release:skill": "node scripts/release.mjs skill",
"release:cli": "node scripts/release.mjs cli",
The Rust engine: one binary replaces every script and the JS detector, fully open (#714) * Add oracle harness: verb goldens and function-level vectors Records stdout/stderr/exit/files for every impeccable verb over a fixed corpus and replays them against an alternate implementation. Adds a loader hook that captures per-function call vectors from the pure engine modules. Prepared with AI assistance (Claude Code). * Oracle: hook, hook-before-edit, hook-admin cases and goldens Prepared with AI assistance (Claude Code). * Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb Prepared with AI assistance (Claude Code). * Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens Prepared with AI assistance (Claude Code). * Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon) Prepared with AI assistance (Claude Code). * Oracle: mask the binary path before HOME; export launcher env to the binary Prepared with AI assistance (Claude Code). * detect: set process.exitCode instead of exiting after the final write process.exit() right after a large piped stdout write truncated JSON output at the pipe buffer boundary; found by the oracle harness. Re-record the six directory-scan goldens that had captured the truncation. Prepared with AI assistance (Claude Code). * Oracle: normalize the hook-admin command in both runtimes' forms and audit chars Prepared with AI assistance (Claude Code). * Skill text: invoke the impeccable launcher instead of node scripts Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>` (context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points Windows shells without sh at impeccable.cmd, and says the launcher runs a self-contained binary. allowed-tools follows. Prepared with AI assistance (Claude Code). * Scripts dir: replace the Node scripts with the impeccable launcher skill/scripts keeps command-metadata.json and the page JS; every .mjs entry point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION), scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir. Prepared with AI assistance (Claude Code). * Build: ship the launcher instead of bundling the JS engine readSourceFiles no longer copies cli/engine into the skill; the scripts payload is the launcher (executable bit preserved through dist, plugin/, and universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every build), the page JS, and command-metadata.json. Hook manifests call `<scripts>/impeccable hook` behind an existence guard (Codex adds a commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit; GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and systemMessage notice are gone. build:release fetches the pinned engine for every target (lenient) and stages bin/<os-arch>/ into the dist skill copies after root harness dirs and plugin/ were synced, so git-delivered trees stay launcher-only. The detection-rule count check reads the vendored extension/detector/antipatterns.json and is skipped when absent. build:browser is a stub; the codex prefix rewrite leaves `{{scripts_path}}/impeccable` alone. Prepared with AI assistance (Claude Code). * CLI: turn the impeccable npm package into a platform-binary shim cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code). * Tests: gate behavior on the oracle and the engine binary Unit tests of the deleted Node scripts and the JS detector are removed; their behavior is pinned by tests/oracle goldens (frozen JS behavior plus reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/, via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework fixture sweep drives live-inject, live-wrap, and detect-csp through the binary the same way. record.mjs learns --bin. The function-level vectors under tests/oracle/vectors/calls are committed as the frozen snapshot they can no longer be regenerated from. Suites: core trimmed to build and transformer tests, oracle added to the default run, detector/live reduced to packaging and reference checks, the live-e2e helper tests move to the opt-in live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder. Prepared with AI assistance (Claude Code). * 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). * Tests: tighten the hook command guard assertion Prepared with AI assistance (Claude Code). * Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md Prepared with AI assistance (Claude Code). * Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). * Tests: drive the live-e2e orchestrator through the engine binary The session, fake-agent loop, steer test, and manual-edit probe spawn <binary> <verb> (live-server, live, live-inject, live-wrap, live-insert, live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs instead of node skill/scripts/live-*.mjs; the completion typing the agent imported from the deleted live/completion.mjs is a small local helper. The live-e2e helper unit tests move back into the default live suite (the steer loop skips without a binary). Prepared with AI assistance (Claude Code). * Tests: run new-work-e2e through the engine's serve-question and generate-image verbs Prepared with AI assistance (Claude Code). * Tests: point the skill-behavior harness at the launcher and engine binary The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs without a download; scenarios assert on 'impeccable context' instead of context.mjs and skip without a binary. Prepared with AI assistance (Claude Code). * Tests: note what plugin-e2e validates before and after the generated-output sync Prepared with AI assistance (Claude Code). * Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta Prepared with AI assistance (Claude Code). * Rebase reconciliation: fold main's post-freeze work into the swapped tree The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). * Oracle: restore detector goldens to post-fix behavior after the engine ports The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid pass, comment stripping, root-relative linked stylesheets, URL userinfo redaction, inert ignore-value refusal) and the comp-fidelity rules organic-clip-path / buried-raster are ported to the engine. Re-records the gap-pinning detect goldens from the fixed binary (glow.html included: its .photo-opaque-grad column now carries the buried-raster finding it was written for), replays the frozen checkHtmlPatterns call vectors through the last JS engine state in history (db1462b9^; args untouched, 14 of 101 results moved), and rewrites the DELTAS gap section into the landed-ports note. Each re-recorded json fixture golden byte-matches that JS state's output; oracle: 770 pass, 0 fail. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope, end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 + 3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and deep findings keep their established artifact order, 80997663). Re-recorded goldens whose old bytes froze pre-fix behavior, with a DELTAS.md entry naming each upstream hash: the Stop finding-cache sync (3c442af7), the Edit|Write manifests without the retired MultiEdit matcher (7d5c60d2), and the failWithRollback field order (1f2c3f9d). Prepared with AI assistance (Claude Code). * Oracle: drop a duplicated DELTAS section The verb-fix section landed twice when two porting sessions staged the same file; keep one copy. Prepared with AI assistance (Claude Code). * Oracle: pin the hooks ignore-value inert-entry refusal Three hadmin-ignore-value-inert-* cases record the engine's port of be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose findings can never extract one is refused with the wildcard-plus-file route (and no config write), while the wildcard scoped form for the same rule is accepted. Goldens recorded from the engine binary and verified byte-for-byte against the ea360025 hook-admin.mjs on the same sequences. No existing golden changes, so no DELTAS entry is owed. Prepared with AI assistance (Claude Code). * Launcher: fail closed on a missing download checksum (engine triage C1) Byte-identical sync of the engine repo's launchers: a freshly downloaded engine binary now runs only after verifying against its .sha256 sidecar. A sidecar that cannot be fetched, or a machine with no sha256 tool, refuses the download instead of exec'ing an unverified binary; the wget-only path fetches the sidecar too. Binaries already on PATH or in the cache that pass engine-probe are unaffected. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Enforce engine-before-skill release order (triage D4) The launcher, npm shim, and `impeccable install` all resolve the engine binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap merge published ahead of the engine release + platform packages dead-ends every install path. Add a mechanical guard: - scripts/check-engine-release.mjs: verifies all five dist binaries + .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero. Honors IMPECCABLE_DOWNLOAD_BASE. - release.mjs: hard-fails release:skill and release:cli when assets are missing; extension is exempt (vendored WASM detector, no engine exec). - CI engine-release-ready job: runs the check, continue-on-error with a loud ::warning until the first engine release exists (flip to false then). - CLAUDE.md Releases: documents the enforced ordering. Prepared with AI assistance (Claude Code). * Oracle: re-record the Sep-1 verb fixes ported to the Rust engine Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4. - detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps): the #678 overused-font primary-face change (a system stack keeps its system face, so a Roboto fallback no longer flags). Upstream 2cfd6076. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: fix pre-existing release-guard staging on the swap branch release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * oracle: pin E8 stale-hook-manifest detector fallback (context) Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: stop two harness hangs from wedging a whole run Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). * launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix) Prepared with AI assistance (Claude Code). * Node-free swap: comp-fidelity verbs move to the engine The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase) and their six libs are ported into the impeccable-engine binary. This removes the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'` now returns nothing. - reference/new-work.md, reference/visualize.md, and the asset-producer / finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead of `node <script>.mjs`. - Deleted the ten ported .mjs and the four JS unit tests that imported them (their behavior is now covered by the engine's Rust tests and the oracle); removed those files from scripts/test-suites.mjs. - Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic workspace, recorded from the engine binary; the deterministic outputs are byte-identical to the JS the scripts left behind. - docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and the runtime-resolved (never-committed) font-index catalog. The font-index catalog JSON stays shipped in the skill (data/font-index.json); the engine resolves it at run time and never vendors it. Prepared with AI assistance (Claude Code). * reorg: public plumbing for the in-repo Rust workspace and the two-release flow The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * reorg C: the open Rust runtime joins this repo as one Cargo workspace The engine no longer lives in a separate repo. `crates/` is a snapshot of the open crates (foundation, core, common, context, live, hook, skills, comp, comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git archive of the engine repo at the commit that finished the boundary split. None of that repo's history comes with it, and none of it should: the closed half stays private. The closed half is the rule engine. It ships as a prebuilt native archive per target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub Release on this repo. `crates/core/build.rs` resolves and links it three ways: `IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the `~/.impeccable/detector/<version>/<target>/` cache, else a download verified against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol C ABI; nothing above it knows the boundary exists. What changed versus the engine repo copy: - Every crate manifest moves from `license-file.workspace` to `license.workspace` (this workspace declares Apache-2.0), and the workspace gains the `postcard` dependency the boundary encoding needs. - The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of a sibling `launcher/` dir, and `engine_binary` downloads from `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead of the retired dist repo. No oracle golden carried the old URL, so no re-recording was owed. - The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`, `../impeccable-second` or a hardcoded home directory now resolve the root as `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an override for an out-of-tree checkout. - The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm glue) is no longer tracked. `crates/core/build.rs` resolves it beside the archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and live mode serves that. `scripts/check-detector-release.mjs` now requires it and its `.sha256` in a detector release. - The live crate embeds `skill/scripts/live-browser*.js` and `modern-screenshot.umd.js` directly rather than through vendored copies, so the binary and the installed skill cannot drift. - `crates/browser/assets/` (an unused second copy of the bundle) is gone. - `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a plain `cargo build --release -p impeccable` is enough to run `bun run test`. Verified with the archive from a local detector build: `cargo test --workspace` 267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the default suite green, and the launcher's `engine-probe` handshake answering through `skill/scripts/impeccable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime They describe the binary's environment contract and the parity method every crate here was ported with; both belong next to the crates now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core/build.rs: refuse a detector archive built by another rustc, in plain words The archive links only against the exact rustc that built it; a mismatch used to surface as pages of undefined std symbols from the linker. The detector repo now writes rustc-version.txt next to the archive (and ships it with the release); when it is present, build.rs compares it with its own compiler and names both versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * build:extension: ship the wasm-core extension shell and vendor its detector from the detector release `bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: replay byte-for-byte on Linux too The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json') Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: darwin-x64 builds on macos-14 (macos-13 is retired) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Open the detector: the rule crates join the workspace, the C-ABI goes away The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core: doc comments drop the open/closed split The rule crate and the foundation crate are both Apache-2.0 in one workspace now, so "open", "closed" and "crosses the boundary" no longer describe anything. Comments only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Rule packs: downstream crates add rules on all three engines; wasm detect surface A crate that depends on this workspace can now add rules without forking it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync + Debug) carries a pack's registry rows plus three hooks that default to empty: `check_text` for the text engine, `check_element_dom` and `check_page_dom` for the browser driver. `impeccable_html::StaticRulePack` adds `check_document` for the static engine, where the document model belongs to the html crate and detect cannot name it. The registry keeps ANTIPATTERNS as the built-in list; `registry::extend` appends a pack's rows and every lookup consults them after the built-ins, so a pack can never shadow a built-in id (extend panics on a collision and is idempotent per slice). `all_antipatterns()` is the built-ins followed by the registered rows. Hook order, chosen so built-in output cannot move: - detect_text: after every matcher, analyzer and the dedupe, before inline ignores, so `impeccable-disable` waives pack rules like built-in ones. - detect_html_source: after the element rules, the design-system merge and the page passes, again before inline ignores. One pack pass per HTML file: the document hook when set, otherwise the text hook over the raw source, so a pack implementing both never reports twice. - collect_browser_findings: the element hook at the end of the per-element loop through the same disabled-rules filter and group, the page hook after every built-in page pass with the same el-or-body attribution. A pack travels on TextOptions / ScanOptions, DetectHtmlOptions (static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig (serde-skipped: a pack is a Rust value, not JSON from the page). The shipped binary installs none. `crates/wasm --features detect` exposes the two file engines as JSON exports for hosts that cannot exec the binary: `detect_text_json` and `detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`, returning the findings array `detect --json` prints. `antipatterns_json` now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are Rust-only, for a crate that links this one as an rlib. Tests: registry extension and collision in foundation, one test pack per engine (crates/core, crates/detect, crates/html tests) proving each hook fires, that the built-in findings are unchanged, and that the waivers and the disabled-rules list cover pack rules, plus the wasm export shapes. Workspace tests 346 to 361, oracle 795/0 unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist under the open design Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * bundle: the page JS and the bundler become a library crate downstream packs can reuse The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * The immediate tier moves to the registry, and reaches wasm The design hook's immediate-tier list is the set of rule ids worth fixing at the edit site, and a downstream reviewer wants the same set to decide how loudly a finding is reported. `impeccable-hook` is native-only, so the list moves to `impeccable_core::registry` (the hook re-exports it) and the `detect` feature gains `immediate_tier_rules_json()`. The export is behind `detect`, which the in-page bundle does not build, so the tracked browser asset is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: Pristine tracks the engine by revision pin, not npm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist is maintainer-side, not part of the tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix flat type hierarchy false positives (#702) Upstream sha 84728e9ce43a3dba2a453b20130bbf836190d77c. The rule now reads rendered semantic roles and the dominant size per role instead of the raw set of font sizes on the page, and it fires only when every adjacent role step is under 1.25x. - crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES / MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and checkFlatTypeHierarchySamples, the shared half of checks.mjs. - crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over the Dom trait, with the overlay skip selector checkTypography passes. - crates/html page.rs gets the same walk over StaticDocument. - crates/detect drops the source-only analyzer: flat-type-hierarchy leaves REGEX_ANALYZERS, the text-content analyzers shift to index 1, and analyzer_rule_id loses its first row. - crates/html cascade defaults gain contentVisibility, and crates/foundation registry carries the reworded description. Goldens re-recorded (the binary now matches origin/main's JS engine on every one of these fixtures, verified by scanning the shared corpus with both): glow, icon-tile-stack, layout, modern-color-borders, motion, named-color-borders, numbered-section-markers, oklch-neon-text, typography-should-flag, json and text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix detector URL scans and advisory handling (#709) Upstream sha fa44839f7289fced3f51946684656a28775638cc. Advisory handling. `severity` becomes the canonical registry field: the `advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on `severity == "advisory"`, and `derive_advisory_flag` stamps the finding's `advisory: true` from the effective severity, so a per-finding promotion or demotion carries the flag. The html and browser engines call it after their severity override; the detect CLI and the hook accept either spelling; the driver's serializer and the wasm registry exports derive it the same way. em-dash-overuse moves from `advisory: true` to `severity: "advisory"`. URL scans. `expand_joined_url_targets` splits an argv value that is entirely whitespace-separated URLs and leaves paths with spaces alone. The browser driver reads the readable linked-stylesheet corpus into the HTML pattern corpora and resolves a finding's selector with `selector_nodes_for_live_dom` / `pseudo_element_host_selector`, so an unresolvable selector drops the finding instead of keeping it page-level. The CSSOM walk itself is page JS: `browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping rules flattened, container-query probes, effective keyframes) and puts it in the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page route, and the Dom trait carries `linked_stylesheet_text`. Also `enclosing_css_selector` blanks comments before hunting the previous declaration delimiter, and `check_typography` reports the uniquely most-used family instead of every family over a 15% share. Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns` is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree over the shared corpus (432 findings). The two changed lines in tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were re-recorded by running origin/main's `checkHtmlPatterns` over the frozen args; only the comment-polluted selector changed. Goldens re-recorded for the advisory partition (config-*, fixture gemini/gpt-tells, numbered-section-labels, scoped-ignore, shape-assembled-illustration, color, em-dash-entities) and the help text, each cross-checked against the JS on origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707) Upstream sha 32b270f4e8ec0af40ef85508c224f0f49096bd7d. `find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex` crate has no lookahead, so the maximal digit run plus the word boundary is matched as before and the byte after it is tested for `/`. The text engine gains the JS-source scanner (`scan_js`) and the scope helpers on top of it: `containing_markup_tag` keeps a gray text class from pairing with a background in a sibling tag on the same line, and `find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b` class expression into its arms, recursing into nested ternaries, ignoring `?.` and `??`, and keeping a common prefix and post-ternary suffix in every arm. `MatchCtx` now carries the match offset the scope lookup needs. Verified against origin/main's JS: all eleven cases from the upstream test file plus a nested / nullish / suffix set produce byte-identical findings on both engines; they are pinned as Rust unit tests in `regex_matchers` and `checks::rules`. The shared fixture corpus stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: resolve unique --target names in monorepos (#706) Upstream sha 8b326fc81e026fffbcdcecd94ce34af956ebea79. `resolve_target_path` / `find_unique_bare_target` in `crates/context`: a `--target` that does not exist and reduces to a single path segment under cwd resolves to the one workspace candidate with that name, so `--target a` selects `apps/a`. A caller that already absolutized the name against cwd (live and the other helpers do) takes the same route. Ambiguous or unknown names still report the miss. The context CLI resolves the target once and hands the resolved path to `load_context`, replacing `path_exists_for_target`. Oracle: four new `context-monorepo-target-bare-*` cases (bare name, absolutized bare name, unknown name, bare name from a child cwd). `context-monorepo-target-b-inherits` was re-recorded: resolving the target before `load_context` changes its `surfaceBriefReason` from `not-found` to `invalid-target`, which is what origin/main's `context.mjs` prints for the same run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Next.js 16 CSP and parent hook discovery (#710) Upstream sha 672ca29642b513bc3365afb0e309fff3d6dfa752. CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook beside `middleware.*`, but only where it sits at a project root or its `src/` directory: the scan root itself, or a nested directory carrying a Next project marker (a `next.config.*`, an `app` / `pages` dir, or a `next` dependency). A same-named helper elsewhere in the tree is not the framework hook. Context. `find_git_boundary_root` gives `resolve_project` a git-boundary notion: an explicit target inside its own repository resolves against that repository, and an external target resolves against its own root, so caller context never leaks across the boundary. `hook_manifest_search_roots` replaces the cwd/projectRoot/repoRoot triple with a walk up from the project root that stops at the first git boundary, and each root's own hook lifecycle config is honored before its manifest counts as coverage. Verified against origin/main's JS: nine `detect-csp` placements and five hook-discovery scenarios (enclosing harness root, that root disabled, sibling target, nested git target, markerless nested git target) produce identical output. Oracle: five `csp-proxy-*` cases and five `context-hook-*` / `context-markerless-nested-git-target` cases. Four route-target goldens were re-recorded because #710 resolves a `/`-prefixed target outside the workspace; each was cross-checked against origin/main, and `surface-brief-write-route` has a DELTAS entry for the one wording difference (an unwritable filesystem root). `tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from the merge importing the deleted `detectCsp`; it now drives `detect-csp` through the binary like the rest of that file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: fail URL scans when the browser is unavailable (#711) Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: OpenCode slash command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78. OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a pinned skill never reaches its slash menu. `pin` now writes `commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and skips `.opencode` in the SKILL.md loop so no unreachable `.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded, and reaches both scopes even when the skill itself is gone. `find_opencode_commands_dirs` covers the project-local dir when the project has the skill and the user config dir when Impeccable is installed globally, resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`). The build-tooling half of the upstream change (transformers, the OpenCode command the build generates, `root-commands-sync`) came in with the merge and needed no port. Verified against origin/main's pin.mjs across seven scenarios (no harness, project scope, user scope, a foreign command file, pin then unpin, unpin over a foreign file, unpin with nothing pinned): identical stdout, identical file sets, identical file contents apart from the one deliberate difference. Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body naming the launcher rather than `node .../context.mjs`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Codex skill version metadata (#703) Upstream sha 482368511ace07982a7cd3a23dd60cf62d6f68c8. Codex's validator rejects unknown top-level keys, so the Codex and `.agents` skills now carry `version` under the spec-defined `metadata:` map. Both version readers learn the same parser: `parse_skill_frontmatter_version` in `crates/context` (the boot update check) and `extract_version` in `crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy top-level one still reads, only the map's own indent level counts, tabs count as two spaces, and a comment line is skipped. The build-tooling half (`versionInMetadata` on the two providers, the YAML emitter's nested-object branch) came in with the merge. Fourteen frontmatter shapes were recorded from origin/main's `parseSkillFrontmatterVersion` and pinned as unit tests in both crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix skill subcommand help handling (#708) Upstream sha a26419917716b16623cc830429f3cc1a4f7cd630. `install`, `link`, `update` and `check` render static help before entering any operational path, through both the top-level verb and the legacy `skills` namespace, for `--help` and `-h` alike. Verified against origin/main's `cli/bin/cli.js`: all six spellings produce identical text and exit codes. Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the help paths are pinned there; every other installer path writes into harness directories or reaches the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle: goldens for the three fixtures the merge added `tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: the installer half of the OpenCode command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78, the part of it that lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`. `copy_provider_commands` mirrors `copy_provider_skills` for a provider's compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`, user scope writes the config dir OpenCode actually scans (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`), and a pre-#406 global install at `~/.opencode/commands/` loses exactly the files just written while siblings, symlinked dirs and home-rooted git repos are left alone. It runs on install, on the reinstall refresh, on update, and on link, which is the only path that can deliver the bridge to a linked install. `is_up_to_date` now compares the bundle's command files too, so an install whose skills match but whose bridge is missing or drifted refreshes instead of reporting success while the slash command stays absent. Only bundle-shipped files are compared, so a pinned shortcut never affects freshness. `tests/copy-provider-commands.test.js` arrived with the merge importing the deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to `crates/skills/tests/provider_commands_tests.rs` (project scope, the three user-scope dir resolutions, the legacy migration and its two guards, a provider with no commands dir, and the four `isUpToDate` command-awareness cases), and the file is removed and deregistered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CI: the first full run on the branch, three fixes - The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context tests: the verbatim-prefix strip spells the prefix once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: derive the snapshot identity from the verb's own resolver Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: JSON-quote the snapshot identity, as the verb does Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * detect test: import resolution against platform-form paths Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills tests pass on Windows The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the kernel takes a verbatim path literally, so every `/`-joined path built under them was an invalid filename. Strip it the way Node's `realpathSync` does. The manifest, artifact and sibling-binary expectations hard-coded POSIX separators for paths the product joins with the host's semantics; derive them from `jsp::join` instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests pass on Windows Same verbatim-prefix strip on the test temp roots, plus expectations derived from the helpers the product uses: cache keys and scan targets from `jsp::join`, the config path in an admin message from the same relative form `path.relative` renders, and the footer hints from `quote_command_arg`, which deliberately switches to the double-quoted Windows form (#476 / #533). The env lock no longer poisons the sibling tests when one of them fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: html oracle goldens compare on Windows The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then render the remainder with `/` so a Windows checkout's backslashes are not read as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: widen the live read-deadline test's margin Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system timer while the crate's tests run in parallel, so the later request takes its turn later there. The bound stays far under the 60s read timeout a deadline-less read would hold the ticket for, so the test still distinguishes the fix from the regression. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests derive the rest of the host path forms The test temp helper's `write` returned a `PathBuf::join` result, which keeps the `/` inside the relative part and so does not match what the hook resolves a relative target to on Windows. Three more admin messages and the cache-root slug pinned the POSIX spelling of paths the product renders with the host's semantics (`path.resolve` also prefixes the current drive there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills test fixtures name USERPROFILE, and the win32 quoted form `os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only HOME sent the global installs into the runner's real profile. The Windows hook command carries the JSON-quoted path, so a host path's backslashes arrive escaped; derive the expectation instead of pinning the POSIX spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the oracle fixtures out with LF A finding's snippet carries the scanned file's own bytes, and the goldens were recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the grok global-install manifests as JSON The Windows hook command carries the JSON-quoted launcher path, so the path's backslashes are escaped once inside the command and again by the manifest file itself. Read the manifest as JSON and look for either quoting form instead of counting escaping layers in a raw substring match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * npm shim: refuse a download with no verifiable sidecar The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle fixture: declare the vite plugin the web workspace imports `live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Vectors: drop the 12,208 byte-identical repeat lines The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Fix: restore the live overlay's disabledValues waivers in the engine The JS engine applied value-level ignore waivers at the tail of collectBrowserFindings: `_disabledValues` read the entries the live overlay resolved for the page (skill/scripts/live-browser-ignores.js sends them as config.disabledValues), and filtered the assembled findings by the value each one reported, with design-system-color compared by color value rather than by spelling so a hex waiver suppressed a finding the browser reported as rgb(...). The Rust port dropped that stage: `disabledValues` appeared nowhere in the workspace or in browser-bundle, so a project entry like [detector] ignoreValues = [{ rule = "overused-font", value = "geist mono" }] stopped reaching the overlay. The rules the CLI and the edit hook waive kept drawing markers and counting toward the badge. Restore it end to end: * BrowserConfig gains `disabled_values`, parsed leniently so a hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped rather than failing the whole config, the way the JS filter did. * The driver applies the waivers after every pass, so a rule pack's findings are covered the same way the built-in ones are, honoring the entries only in extension mode exactly as the JS read them. The normalizer, the value extractor (including the rule that bounce-easing without a direct ignoreValue offers no value) and the hex/rgb color key are ported alongside it. * collectConfigJson in the in-page bundle and configJson in the offscreen bundle forward the field. The extension never sends it, so its behavior is unchanged. Coverage: two driver unit tests (suppression by font value, by hex waiver across the rgb spelling, and the extension-mode gate; plus the config parse and the normalizers), a skipScan test that pins the empty shape for every stage the core produces, and crates/wasm/tools/disabled-values-check.mjs, a browser-backed check ported from the retired tests/detect-antipatterns-browser.test.mjs case that the swap left without a replacement. Against the previous bundle it fails on exactly the three waiver assertions and passes the skipScan one, which is the shape of the regression. Two related review findings were checked and are not defects. skipScan is gated on extension mode in both the driver and the bundle, which is what the JS did (index.mjs#skipScanActive), and the live overlay runs in extension mode: live-browser.js sets `s.dataset.impeccableExtension` on the injected /detect.js tag, and the overlay's whole detect toggle travels over the postMessage loop that 50-scan.js installs only under EXTENSION_MODE. The visual contrast stage is not leaking either: collectBrowserFindingsAsync and scan() both consult skipScanActive(), and the offscreen path skips its visual pass on config.skipScan. The tracked live asset is regenerated (cargo xtask bundle). The oracle replays with zero unreviewed differences: the new field defaults empty and the filter is inert without it, and no CLI path sets extension mode. AI-assisted change: implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Shim test: run from a staged copy and prove the download happened The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the loader now hands off when the resume is the arrival The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live server: stop ends the process, SSE skips the mutation lane Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the shader teardown can no longer race its own construction The new cycling assertion caught a real defect on CI: vite8-react-insert reached CYCLING with #impeccable-live-shader still painted over the page. showShaderOverlay is async. It appends its canvas synchronously, then awaits createImageBitmap and finishes the GL setup before it publishes shaderState. hideShaderOverlay returned early on a null shaderState, so a teardown that landed inside that window did nothing, and the construction then published itself over a session that had already left GENERATING, with no teardown left to run. The scroll tick kept repositioning it, which is why the CI page.html shows the canvas sized from the capture rect but styled to the cycling anchor. Every teardown now bumps a shader epoch before it does anything else, and a construction pins the epoch it owns and abandons its canvas (releasing the GL context) at every point past an await and before any publish, including both bitmap-fallback publishes. A teardown also drops a shader node that no shaderState owns, so an already-orphaned canvas cannot survive one. Reproduced by widening the append-to-publish window: with a 400ms delay after uiAppend, vite8-react-insert failed with the CI error and the probe showed the teardown arriving at CYCLING with shaderState still null. The same run passes with this change, as does a 1500ms window on insert and plain. Locally that window is about 4ms, which is why it only showed on a slower runner. The four remaining setLiveState('CYCLING') sites that did not lower the loader now do: the SSE done handler (the one route that can reach CYCLING from GENERATING), the Svelte republish remount, and the two accept failure recoveries. The e2e assertion already waits up to 5s for the shader to clear, so it was never racing a legitimate teardown; it is left as it is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: every active-session wrapper lookup goes through the resolver Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: a discard releases every wrapper it hid Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Release: publish the npm platform packages in one command bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: pin checkout, upload-artifact and download-artifact at v7 The v4 pins target Node 20, which the runner now deprecates and forces onto Node 24 with a warning on every step. The rest of the workflows already use v7. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: make the temp-dir helpers unique under a coarse clock Windows' system clock is coarse enough that two parallel tests could get the same pid-plus-nanoseconds directory name and then remove each other's files (rust-windows: close_verb_round_trip_and_ownership, NotFound). A per-process counter is appended to the name. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: declare the temp-dir counter in the hook cache-root tests The previous commit referenced TMP_SEQ there without defining it. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-09-04 10:42:45 -07:00
"release:ext": "node scripts/release.mjs extension",
"release:engine": "node scripts/release.mjs engine",
"release:platform-packages": "node scripts/publish-platform-packages.mjs",
"check:engine-release": "node scripts/check-engine-release.mjs"
},
"optionalDependencies": {
"@impeccable/cli-darwin-arm64": "0.1.5",
"@impeccable/cli-darwin-x64": "0.1.5",
"@impeccable/cli-linux-x64": "0.1.5",
"@impeccable/cli-linux-arm64": "0.1.5",
"@impeccable/cli-windows-x64": "0.1.5"
},
"devDependencies": {
"@ai-sdk/anthropic": "^4.0.7",
"@ai-sdk/google": "^4.0.8",
"@ai-sdk/openai": "^4.0.7",
chore(deps-dev): bump the bun-minor-and-patch group with 11 updates (#207) Bumps the bun-minor-and-patch group with 11 updates: | Package | From | To | | --- | --- | --- | | [@ai-sdk/anthropic](https://github.com/vercel/ai/tree/HEAD/packages/anthropic) | `3.0.71` | `3.0.81` | | [@ai-sdk/google](https://github.com/vercel/ai/tree/HEAD/packages/google) | `3.0.75` | `3.0.80` | | [@ai-sdk/openai](https://github.com/vercel/ai/tree/HEAD/packages/openai) | `3.0.53` | `3.0.68` | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.2.119` | `0.3.165` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.91.1` | `0.101.0` | | [ai](https://github.com/vercel/ai/tree/HEAD/packages/ai) | `6.0.168` | `6.0.197` | | [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) | `6.2.1` | `6.4.4` | | [motion](https://github.com/motiondivision/motion) | `12.38.0` | `12.40.0` | | [playwright](https://github.com/microsoft/playwright) | `1.59.1` | `1.60.0` | | [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler) | `4.85.0` | `4.98.0` | | [zod](https://github.com/colinhacks/zod) | `4.3.6` | `4.4.3` | Updates `@ai-sdk/anthropic` from 3.0.71 to 3.0.81 - [Release notes](https://github.com/vercel/ai/releases) - [Changelog](https://github.com/vercel/ai/blob/@ai-sdk/anthropic@3.0.81/packages/anthropic/CHANGELOG.md) - [Commits](https://github.com/vercel/ai/commits/@ai-sdk/anthropic@3.0.81/packages/anthropic) Updates `@ai-sdk/google` from 3.0.75 to 3.0.80 - [Release notes](https://github.com/vercel/ai/releases) - [Changelog](https://github.com/vercel/ai/blob/@ai-sdk/google@3.0.80/packages/google/CHANGELOG.md) - [Commits](https://github.com/vercel/ai/commits/@ai-sdk/google@3.0.80/packages/google) Updates `@ai-sdk/openai` from 3.0.53 to 3.0.68 - [Release notes](https://github.com/vercel/ai/releases) - [Changelog](https://github.com/vercel/ai/blob/@ai-sdk/openai@3.0.68/packages/openai/CHANGELOG.md) - [Commits](https://github.com/vercel/ai/commits/@ai-sdk/openai@3.0.68/packages/openai) Updates `@anthropic-ai/claude-agent-sdk` from 0.2.119 to 0.3.165 - [Release notes](https://github.com/anthropics/claude-agent-sdk-typescript/releases) - [Changelog](https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md) - [Commits](https://github.com/anthropics/claude-agent-sdk-typescript/compare/v0.2.119...v0.3.165) Updates `@anthropic-ai/sdk` from 0.91.1 to 0.101.0 - [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases) - [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md) - [Commits](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.91.1...sdk-v0.101.0) Updates `ai` from 6.0.168 to 6.0.197 - [Release notes](https://github.com/vercel/ai/releases) - [Changelog](https://github.com/vercel/ai/blob/ai@6.0.197/packages/ai/CHANGELOG.md) - [Commits](https://github.com/vercel/ai/commits/ai@6.0.197/packages/ai) Updates `astro` from 6.2.1 to 6.4.4 - [Release notes](https://github.com/withastro/astro/releases) - [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md) - [Commits](https://github.com/withastro/astro/commits/astro@6.4.4/packages/astro) Updates `motion` from 12.38.0 to 12.40.0 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v12.38.0...v12.40.0) Updates `playwright` from 1.59.1 to 1.60.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0) Updates `wrangler` from 4.85.0 to 4.98.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.98.0/packages/wrangler) Updates `zod` from 4.3.6 to 4.4.3 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.3.6...v4.4.3) --- updated-dependencies: - dependency-name: "@ai-sdk/anthropic" dependency-version: 3.0.81 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: bun-minor-and-patch - dependency-name: "@ai-sdk/google" dependency-version: 3.0.80 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: bun-minor-and-patch - dependency-name: "@ai-sdk/openai" dependency-version: 3.0.68 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: bun-minor-and-patch - dependency-name: "@anthropic-ai/claude-agent-sdk" dependency-version: 0.3.165 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: "@anthropic-ai/sdk" dependency-version: 0.101.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: ai dependency-version: 6.0.197 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: bun-minor-and-patch - dependency-name: astro dependency-version: 6.4.4 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: motion dependency-version: 12.40.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: playwright dependency-version: 1.60.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: wrangler dependency-version: 4.98.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch - dependency-name: zod dependency-version: 4.4.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: bun-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 17:29:56 -07:00
"@anthropic-ai/claude-agent-sdk": "^0.3.165",
"@anthropic-ai/sdk": "^0.123.0",
"@babel/parser": "^8.0.4",
"ai": "^7.0.14",
"archiver": "^8.0.0",
"playwright": "^1.59.1",
The Rust engine: one binary replaces every script and the JS detector, fully open (#714) * Add oracle harness: verb goldens and function-level vectors Records stdout/stderr/exit/files for every impeccable verb over a fixed corpus and replays them against an alternate implementation. Adds a loader hook that captures per-function call vectors from the pure engine modules. Prepared with AI assistance (Claude Code). * Oracle: hook, hook-before-edit, hook-admin cases and goldens Prepared with AI assistance (Claude Code). * Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb Prepared with AI assistance (Claude Code). * Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens Prepared with AI assistance (Claude Code). * Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon) Prepared with AI assistance (Claude Code). * Oracle: mask the binary path before HOME; export launcher env to the binary Prepared with AI assistance (Claude Code). * detect: set process.exitCode instead of exiting after the final write process.exit() right after a large piped stdout write truncated JSON output at the pipe buffer boundary; found by the oracle harness. Re-record the six directory-scan goldens that had captured the truncation. Prepared with AI assistance (Claude Code). * Oracle: normalize the hook-admin command in both runtimes' forms and audit chars Prepared with AI assistance (Claude Code). * Skill text: invoke the impeccable launcher instead of node scripts Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>` (context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points Windows shells without sh at impeccable.cmd, and says the launcher runs a self-contained binary. allowed-tools follows. Prepared with AI assistance (Claude Code). * Scripts dir: replace the Node scripts with the impeccable launcher skill/scripts keeps command-metadata.json and the page JS; every .mjs entry point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION), scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir. Prepared with AI assistance (Claude Code). * Build: ship the launcher instead of bundling the JS engine readSourceFiles no longer copies cli/engine into the skill; the scripts payload is the launcher (executable bit preserved through dist, plugin/, and universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every build), the page JS, and command-metadata.json. Hook manifests call `<scripts>/impeccable hook` behind an existence guard (Codex adds a commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit; GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and systemMessage notice are gone. build:release fetches the pinned engine for every target (lenient) and stages bin/<os-arch>/ into the dist skill copies after root harness dirs and plugin/ were synced, so git-delivered trees stay launcher-only. The detection-rule count check reads the vendored extension/detector/antipatterns.json and is skipped when absent. build:browser is a stub; the codex prefix rewrite leaves `{{scripts_path}}/impeccable` alone. Prepared with AI assistance (Claude Code). * CLI: turn the impeccable npm package into a platform-binary shim cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code). * Tests: gate behavior on the oracle and the engine binary Unit tests of the deleted Node scripts and the JS detector are removed; their behavior is pinned by tests/oracle goldens (frozen JS behavior plus reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/, via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework fixture sweep drives live-inject, live-wrap, and detect-csp through the binary the same way. record.mjs learns --bin. The function-level vectors under tests/oracle/vectors/calls are committed as the frozen snapshot they can no longer be regenerated from. Suites: core trimmed to build and transformer tests, oracle added to the default run, detector/live reduced to packaging and reference checks, the live-e2e helper tests move to the opt-in live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder. Prepared with AI assistance (Claude Code). * 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). * Tests: tighten the hook command guard assertion Prepared with AI assistance (Claude Code). * Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md Prepared with AI assistance (Claude Code). * Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). * Tests: drive the live-e2e orchestrator through the engine binary The session, fake-agent loop, steer test, and manual-edit probe spawn <binary> <verb> (live-server, live, live-inject, live-wrap, live-insert, live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs instead of node skill/scripts/live-*.mjs; the completion typing the agent imported from the deleted live/completion.mjs is a small local helper. The live-e2e helper unit tests move back into the default live suite (the steer loop skips without a binary). Prepared with AI assistance (Claude Code). * Tests: run new-work-e2e through the engine's serve-question and generate-image verbs Prepared with AI assistance (Claude Code). * Tests: point the skill-behavior harness at the launcher and engine binary The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs without a download; scenarios assert on 'impeccable context' instead of context.mjs and skip without a binary. Prepared with AI assistance (Claude Code). * Tests: note what plugin-e2e validates before and after the generated-output sync Prepared with AI assistance (Claude Code). * Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta Prepared with AI assistance (Claude Code). * Rebase reconciliation: fold main's post-freeze work into the swapped tree The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). * Oracle: restore detector goldens to post-fix behavior after the engine ports The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid pass, comment stripping, root-relative linked stylesheets, URL userinfo redaction, inert ignore-value refusal) and the comp-fidelity rules organic-clip-path / buried-raster are ported to the engine. Re-records the gap-pinning detect goldens from the fixed binary (glow.html included: its .photo-opaque-grad column now carries the buried-raster finding it was written for), replays the frozen checkHtmlPatterns call vectors through the last JS engine state in history (db1462b9^; args untouched, 14 of 101 results moved), and rewrites the DELTAS gap section into the landed-ports note. Each re-recorded json fixture golden byte-matches that JS state's output; oracle: 770 pass, 0 fail. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope, end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 + 3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and deep findings keep their established artifact order, 80997663). Re-recorded goldens whose old bytes froze pre-fix behavior, with a DELTAS.md entry naming each upstream hash: the Stop finding-cache sync (3c442af7), the Edit|Write manifests without the retired MultiEdit matcher (7d5c60d2), and the failWithRollback field order (1f2c3f9d). Prepared with AI assistance (Claude Code). * Oracle: drop a duplicated DELTAS section The verb-fix section landed twice when two porting sessions staged the same file; keep one copy. Prepared with AI assistance (Claude Code). * Oracle: pin the hooks ignore-value inert-entry refusal Three hadmin-ignore-value-inert-* cases record the engine's port of be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose findings can never extract one is refused with the wildcard-plus-file route (and no config write), while the wildcard scoped form for the same rule is accepted. Goldens recorded from the engine binary and verified byte-for-byte against the ea360025 hook-admin.mjs on the same sequences. No existing golden changes, so no DELTAS entry is owed. Prepared with AI assistance (Claude Code). * Launcher: fail closed on a missing download checksum (engine triage C1) Byte-identical sync of the engine repo's launchers: a freshly downloaded engine binary now runs only after verifying against its .sha256 sidecar. A sidecar that cannot be fetched, or a machine with no sha256 tool, refuses the download instead of exec'ing an unverified binary; the wget-only path fetches the sidecar too. Binaries already on PATH or in the cache that pass engine-probe are unaffected. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Enforce engine-before-skill release order (triage D4) The launcher, npm shim, and `impeccable install` all resolve the engine binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap merge published ahead of the engine release + platform packages dead-ends every install path. Add a mechanical guard: - scripts/check-engine-release.mjs: verifies all five dist binaries + .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero. Honors IMPECCABLE_DOWNLOAD_BASE. - release.mjs: hard-fails release:skill and release:cli when assets are missing; extension is exempt (vendored WASM detector, no engine exec). - CI engine-release-ready job: runs the check, continue-on-error with a loud ::warning until the first engine release exists (flip to false then). - CLAUDE.md Releases: documents the enforced ordering. Prepared with AI assistance (Claude Code). * Oracle: re-record the Sep-1 verb fixes ported to the Rust engine Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4. - detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps): the #678 overused-font primary-face change (a system stack keeps its system face, so a Roboto fallback no longer flags). Upstream 2cfd6076. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: fix pre-existing release-guard staging on the swap branch release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * oracle: pin E8 stale-hook-manifest detector fallback (context) Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx * Tests: stop two harness hangs from wedging a whole run Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). * launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix) Prepared with AI assistance (Claude Code). * Node-free swap: comp-fidelity verbs move to the engine The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase) and their six libs are ported into the impeccable-engine binary. This removes the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'` now returns nothing. - reference/new-work.md, reference/visualize.md, and the asset-producer / finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead of `node <script>.mjs`. - Deleted the ten ported .mjs and the four JS unit tests that imported them (their behavior is now covered by the engine's Rust tests and the oracle); removed those files from scripts/test-suites.mjs. - Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic workspace, recorded from the engine binary; the deterministic outputs are byte-identical to the JS the scripts left behind. - docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and the runtime-resolved (never-committed) font-index catalog. The font-index catalog JSON stays shipped in the skill (data/font-index.json); the engine resolves it at run time and never vendors it. Prepared with AI assistance (Claude Code). * reorg: public plumbing for the in-repo Rust workspace and the two-release flow The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * reorg C: the open Rust runtime joins this repo as one Cargo workspace The engine no longer lives in a separate repo. `crates/` is a snapshot of the open crates (foundation, core, common, context, live, hook, skills, comp, comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git archive of the engine repo at the commit that finished the boundary split. None of that repo's history comes with it, and none of it should: the closed half stays private. The closed half is the rule engine. It ships as a prebuilt native archive per target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub Release on this repo. `crates/core/build.rs` resolves and links it three ways: `IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the `~/.impeccable/detector/<version>/<target>/` cache, else a download verified against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol C ABI; nothing above it knows the boundary exists. What changed versus the engine repo copy: - Every crate manifest moves from `license-file.workspace` to `license.workspace` (this workspace declares Apache-2.0), and the workspace gains the `postcard` dependency the boundary encoding needs. - The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of a sibling `launcher/` dir, and `engine_binary` downloads from `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead of the retired dist repo. No oracle golden carried the old URL, so no re-recording was owed. - The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`, `../impeccable-second` or a hardcoded home directory now resolve the root as `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an override for an out-of-tree checkout. - The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm glue) is no longer tracked. `crates/core/build.rs` resolves it beside the archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and live mode serves that. `scripts/check-detector-release.mjs` now requires it and its `.sha256` in a detector release. - The live crate embeds `skill/scripts/live-browser*.js` and `modern-screenshot.umd.js` directly rather than through vendored copies, so the binary and the installed skill cannot drift. - `crates/browser/assets/` (an unused second copy of the bundle) is gone. - `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a plain `cargo build --release -p impeccable` is enough to run `bun run test`. Verified with the archive from a local detector build: `cargo test --workspace` 267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the default suite green, and the launcher's `engine-probe` handshake answering through `skill/scripts/impeccable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime They describe the binary's environment contract and the parity method every crate here was ported with; both belong next to the crates now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core/build.rs: refuse a detector archive built by another rustc, in plain words The archive links only against the exact rustc that built it; a mismatch used to surface as pages of undefined std symbols from the linker. The detector repo now writes rustc-version.txt next to the archive (and ships it with the release); when it is present, build.rs compares it with its own compiler and names both versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * build:extension: ship the wasm-core extension shell and vendor its detector from the detector release `bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: replay byte-for-byte on Linux too The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json') Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: darwin-x64 builds on macos-14 (macos-13 is retired) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Open the detector: the rule crates join the workspace, the C-ABI goes away The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * core: doc comments drop the open/closed split The rule crate and the foundation crate are both Apache-2.0 in one workspace now, so "open", "closed" and "crosses the boundary" no longer describe anything. Comments only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Rule packs: downstream crates add rules on all three engines; wasm detect surface A crate that depends on this workspace can now add rules without forking it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync + Debug) carries a pack's registry rows plus three hooks that default to empty: `check_text` for the text engine, `check_element_dom` and `check_page_dom` for the browser driver. `impeccable_html::StaticRulePack` adds `check_document` for the static engine, where the document model belongs to the html crate and detect cannot name it. The registry keeps ANTIPATTERNS as the built-in list; `registry::extend` appends a pack's rows and every lookup consults them after the built-ins, so a pack can never shadow a built-in id (extend panics on a collision and is idempotent per slice). `all_antipatterns()` is the built-ins followed by the registered rows. Hook order, chosen so built-in output cannot move: - detect_text: after every matcher, analyzer and the dedupe, before inline ignores, so `impeccable-disable` waives pack rules like built-in ones. - detect_html_source: after the element rules, the design-system merge and the page passes, again before inline ignores. One pack pass per HTML file: the document hook when set, otherwise the text hook over the raw source, so a pack implementing both never reports twice. - collect_browser_findings: the element hook at the end of the per-element loop through the same disabled-rules filter and group, the page hook after every built-in page pass with the same el-or-body attribution. A pack travels on TextOptions / ScanOptions, DetectHtmlOptions (static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig (serde-skipped: a pack is a Rust value, not JSON from the page). The shipped binary installs none. `crates/wasm --features detect` exposes the two file engines as JSON exports for hosts that cannot exec the binary: `detect_text_json` and `detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`, returning the findings array `detect --json` prints. `antipatterns_json` now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are Rust-only, for a crate that links this one as an rlib. Tests: registry extension and collision in foundation, one test pack per engine (crates/core, crates/detect, crates/html tests) proving each hook fires, that the built-in findings are unchanged, and that the waivers and the disabled-rules list cover pack rules, plus the wasm export shapes. Workspace tests 346 to 361, oracle 795/0 unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist under the open design Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * bundle: the page JS and the bundler become a library crate downstream packs can reuse The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * The immediate tier moves to the registry, and reaches wasm The design hook's immediate-tier list is the set of rule ids worth fixing at the edit site, and a downstream reviewer wants the same set to decide how loudly a finding is reported. `impeccable-hook` is native-only, so the list moves to `impeccable_core::registry` (the hook re-exports it) and the `detect` feature gains `immediate_tier_rules_json()`. The export is behind `detect`, which the in-page bundle does not build, so the tracked browser asset is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: Pristine tracks the engine by revision pin, not npm Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * docs: the cutover checklist is maintainer-side, not part of the tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix flat type hierarchy false positives (#702) Upstream sha 84728e9ce43a3dba2a453b20130bbf836190d77c. The rule now reads rendered semantic roles and the dominant size per role instead of the raw set of font sizes on the page, and it fires only when every adjacent role step is under 1.25x. - crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES / MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and checkFlatTypeHierarchySamples, the shared half of checks.mjs. - crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over the Dom trait, with the overlay skip selector checkTypography passes. - crates/html page.rs gets the same walk over StaticDocument. - crates/detect drops the source-only analyzer: flat-type-hierarchy leaves REGEX_ANALYZERS, the text-content analyzers shift to index 1, and analyzer_rule_id loses its first row. - crates/html cascade defaults gain contentVisibility, and crates/foundation registry carries the reworded description. Goldens re-recorded (the binary now matches origin/main's JS engine on every one of these fixtures, verified by scanning the shared corpus with both): glow, icon-tile-stack, layout, modern-color-borders, motion, named-color-borders, numbered-section-markers, oklch-neon-text, typography-should-flag, json and text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix detector URL scans and advisory handling (#709) Upstream sha fa44839f7289fced3f51946684656a28775638cc. Advisory handling. `severity` becomes the canonical registry field: the `advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on `severity == "advisory"`, and `derive_advisory_flag` stamps the finding's `advisory: true` from the effective severity, so a per-finding promotion or demotion carries the flag. The html and browser engines call it after their severity override; the detect CLI and the hook accept either spelling; the driver's serializer and the wasm registry exports derive it the same way. em-dash-overuse moves from `advisory: true` to `severity: "advisory"`. URL scans. `expand_joined_url_targets` splits an argv value that is entirely whitespace-separated URLs and leaves paths with spaces alone. The browser driver reads the readable linked-stylesheet corpus into the HTML pattern corpora and resolves a finding's selector with `selector_nodes_for_live_dom` / `pseudo_element_host_selector`, so an unresolvable selector drops the finding instead of keeping it page-level. The CSSOM walk itself is page JS: `browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping rules flattened, container-query probes, effective keyframes) and puts it in the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page route, and the Dom trait carries `linked_stylesheet_text`. Also `enclosing_css_selector` blanks comments before hunting the previous declaration delimiter, and `check_typography` reports the uniquely most-used family instead of every family over a 15% share. Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns` is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree over the shared corpus (432 findings). The two changed lines in tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were re-recorded by running origin/main's `checkHtmlPatterns` over the frozen args; only the comment-polluted selector changed. Goldens re-recorded for the advisory partition (config-*, fixture gemini/gpt-tells, numbered-section-labels, scoped-ignore, shape-assembled-illustration, color, em-dash-entities) and the help text, each cross-checked against the JS on origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707) Upstream sha 32b270f4e8ec0af40ef85508c224f0f49096bd7d. `find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex` crate has no lookahead, so the maximal digit run plus the word boundary is matched as before and the byte after it is tested for `/`. The text engine gains the JS-source scanner (`scan_js`) and the scope helpers on top of it: `containing_markup_tag` keeps a gray text class from pairing with a background in a sibling tag on the same line, and `find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b` class expression into its arms, recursing into nested ternaries, ignoring `?.` and `??`, and keeping a common prefix and post-ternary suffix in every arm. `MatchCtx` now carries the match offset the scope lookup needs. Verified against origin/main's JS: all eleven cases from the upstream test file plus a nested / nullish / suffix set produce byte-identical findings on both engines; they are pinned as Rust unit tests in `regex_matchers` and `checks::rules`. The shared fixture corpus stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: resolve unique --target names in monorepos (#706) Upstream sha 8b326fc81e026fffbcdcecd94ce34af956ebea79. `resolve_target_path` / `find_unique_bare_target` in `crates/context`: a `--target` that does not exist and reduces to a single path segment under cwd resolves to the one workspace candidate with that name, so `--target a` selects `apps/a`. A caller that already absolutized the name against cwd (live and the other helpers do) takes the same route. Ambiguous or unknown names still report the miss. The context CLI resolves the target once and hands the resolved path to `load_context`, replacing `path_exists_for_target`. Oracle: four new `context-monorepo-target-bare-*` cases (bare name, absolutized bare name, unknown name, bare name from a child cwd). `context-monorepo-target-b-inherits` was re-recorded: resolving the target before `load_context` changes its `surfaceBriefReason` from `not-found` to `invalid-target`, which is what origin/main's `context.mjs` prints for the same run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Next.js 16 CSP and parent hook discovery (#710) Upstream sha 672ca29642b513bc3365afb0e309fff3d6dfa752. CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook beside `middleware.*`, but only where it sits at a project root or its `src/` directory: the scan root itself, or a nested directory carrying a Next project marker (a `next.config.*`, an `app` / `pages` dir, or a `next` dependency). A same-named helper elsewhere in the tree is not the framework hook. Context. `find_git_boundary_root` gives `resolve_project` a git-boundary notion: an explicit target inside its own repository resolves against that repository, and an external target resolves against its own root, so caller context never leaks across the boundary. `hook_manifest_search_roots` replaces the cwd/projectRoot/repoRoot triple with a walk up from the project root that stops at the first git boundary, and each root's own hook lifecycle config is honored before its manifest counts as coverage. Verified against origin/main's JS: nine `detect-csp` placements and five hook-discovery scenarios (enclosing harness root, that root disabled, sibling target, nested git target, markerless nested git target) produce identical output. Oracle: five `csp-proxy-*` cases and five `context-hook-*` / `context-markerless-nested-git-target` cases. Four route-target goldens were re-recorded because #710 resolves a `/`-prefixed target outside the workspace; each was cross-checked against origin/main, and `surface-brief-write-route` has a DELTAS entry for the one wording difference (an unwritable filesystem root). `tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from the merge importing the deleted `detectCsp`; it now drives `detect-csp` through the binary like the rest of that file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: fail URL scans when the browser is unavailable (#711) Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: OpenCode slash command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78. OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a pinned skill never reaches its slash menu. `pin` now writes `commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and skips `.opencode` in the SKILL.md loop so no unreachable `.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded, and reaches both scopes even when the skill itself is gone. `find_opencode_commands_dirs` covers the project-local dir when the project has the skill and the user config dir when Impeccable is installed globally, resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`). The build-tooling half of the upstream change (transformers, the OpenCode command the build generates, `root-commands-sync`) came in with the merge and needed no port. Verified against origin/main's pin.mjs across seven scenarios (no harness, project scope, user scope, a foreign command file, pin then unpin, unpin over a foreign file, unpin with nothing pinned): identical stdout, identical file sets, identical file contents apart from the one deliberate difference. Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body naming the launcher rather than `node .../context.mjs`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix Codex skill version metadata (#703) Upstream sha 482368511ace07982a7cd3a23dd60cf62d6f68c8. Codex's validator rejects unknown top-level keys, so the Codex and `.agents` skills now carry `version` under the spec-defined `metadata:` map. Both version readers learn the same parser: `parse_skill_frontmatter_version` in `crates/context` (the boot update check) and `extract_version` in `crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy top-level one still reads, only the map's own indent level counts, tabs count as two spaces, and a comment line is skipped. The build-tooling half (`versionInMetadata` on the two providers, the YAML emitter's nested-object branch) came in with the merge. Fourteen frontmatter shapes were recorded from origin/main's `parseSkillFrontmatterVersion` and pinned as unit tests in both crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: Fix skill subcommand help handling (#708) Upstream sha a26419917716b16623cc830429f3cc1a4f7cd630. `install`, `link`, `update` and `check` render static help before entering any operational path, through both the top-level verb and the legacy `skills` namespace, for `--help` and `-h` alike. Verified against origin/main's `cli/bin/cli.js`: all six spellings produce identical text and exit codes. Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the help paths are pinned there; every other installer path writes into harness directories or reaches the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle: goldens for the three fixtures the merge added `tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Port: the installer half of the OpenCode command bridge (#483) Upstream sha 9736a9f6e91bf2f532cafca8f3886df833cd5a78, the part of it that lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`. `copy_provider_commands` mirrors `copy_provider_skills` for a provider's compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`, user scope writes the config dir OpenCode actually scans (`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`), and a pre-#406 global install at `~/.opencode/commands/` loses exactly the files just written while siblings, symlinked dirs and home-rooted git repos are left alone. It runs on install, on the reinstall refresh, on update, and on link, which is the only path that can deliver the bridge to a linked install. `is_up_to_date` now compares the bundle's command files too, so an install whose skills match but whose bridge is missing or drifted refreshes instead of reporting success while the slash command stays absent. Only bundle-shipped files are compared, so a pinned shortcut never affects freshness. `tests/copy-provider-commands.test.js` arrived with the merge importing the deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to `crates/skills/tests/provider_commands_tests.rs` (project scope, the three user-scope dir resolutions, the legacy migration and its two guards, a provider with no commands dir, and the four `isUpToDate` command-awareness cases), and the file is removed and deregistered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CI: the first full run on the branch, three fixes - The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context tests: the verbatim-prefix strip spells the prefix once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: derive the snapshot identity from the verb's own resolver Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * context test: JSON-quote the snapshot identity, as the verb does Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * detect test: import resolution against platform-form paths Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills tests pass on Windows The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the kernel takes a verbatim path literally, so every `/`-joined path built under them was an invalid filename. Strip it the way Node's `realpathSync` does. The manifest, artifact and sibling-binary expectations hard-coded POSIX separators for paths the product joins with the host's semantics; derive them from `jsp::join` instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests pass on Windows Same verbatim-prefix strip on the test temp roots, plus expectations derived from the helpers the product uses: cache keys and scan targets from `jsp::join`, the config path in an admin message from the same relative form `path.relative` renders, and the footer hints from `quote_command_arg`, which deliberately switches to the double-quoted Windows form (#476 / #533). The env lock no longer poisons the sibling tests when one of them fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: html oracle goldens compare on Windows The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then render the remainder with `/` so a Windows checkout's backslashes are not read as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: widen the live read-deadline test's margin Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system timer while the crate's tests run in parallel, so the later request takes its turn later there. The bound stays far under the 60s read timeout a deadline-less read would hold the ticket for, so the test still distinguishes the fix from the regression. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: hook tests derive the rest of the host path forms The test temp helper's `write` returned a `PathBuf::join` result, which keeps the `/` inside the relative part and so does not match what the hook resolves a relative target to on Windows. Three more admin messages and the cache-root slug pinned the POSIX spelling of paths the product renders with the host's semantics (`path.resolve` also prefixes the current drive there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: skills test fixtures name USERPROFILE, and the win32 quoted form `os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only HOME sent the global installs into the runner's real profile. The Windows hook command carries the JSON-quoted path, so a host path's backslashes arrive escaped; derive the expectation instead of pinning the POSIX spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the oracle fixtures out with LF A finding's snippet carries the scanned file's own bytes, and the goldens were recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads as a finding difference. The goldens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * windows: check the grok global-install manifests as JSON The Windows hook command carries the JSON-quoted launcher path, so the path's backslashes are escaped once inside the command and again by the manifest file itself. Read the manifest as JSON and look for either quoting form instead of counting escaping layers in a raw substring match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * npm shim: refuse a download with no verifiable sidecar The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Oracle fixture: declare the vite plugin the web workspace imports `live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Vectors: drop the 12,208 byte-identical repeat lines The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Fix: restore the live overlay's disabledValues waivers in the engine The JS engine applied value-level ignore waivers at the tail of collectBrowserFindings: `_disabledValues` read the entries the live overlay resolved for the page (skill/scripts/live-browser-ignores.js sends them as config.disabledValues), and filtered the assembled findings by the value each one reported, with design-system-color compared by color value rather than by spelling so a hex waiver suppressed a finding the browser reported as rgb(...). The Rust port dropped that stage: `disabledValues` appeared nowhere in the workspace or in browser-bundle, so a project entry like [detector] ignoreValues = [{ rule = "overused-font", value = "geist mono" }] stopped reaching the overlay. The rules the CLI and the edit hook waive kept drawing markers and counting toward the badge. Restore it end to end: * BrowserConfig gains `disabled_values`, parsed leniently so a hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped rather than failing the whole config, the way the JS filter did. * The driver applies the waivers after every pass, so a rule pack's findings are covered the same way the built-in ones are, honoring the entries only in extension mode exactly as the JS read them. The normalizer, the value extractor (including the rule that bounce-easing without a direct ignoreValue offers no value) and the hex/rgb color key are ported alongside it. * collectConfigJson in the in-page bundle and configJson in the offscreen bundle forward the field. The extension never sends it, so its behavior is unchanged. Coverage: two driver unit tests (suppression by font value, by hex waiver across the rgb spelling, and the extension-mode gate; plus the config parse and the normalizers), a skipScan test that pins the empty shape for every stage the core produces, and crates/wasm/tools/disabled-values-check.mjs, a browser-backed check ported from the retired tests/detect-antipatterns-browser.test.mjs case that the swap left without a replacement. Against the previous bundle it fails on exactly the three waiver assertions and passes the skipScan one, which is the shape of the regression. Two related review findings were checked and are not defects. skipScan is gated on extension mode in both the driver and the bundle, which is what the JS did (index.mjs#skipScanActive), and the live overlay runs in extension mode: live-browser.js sets `s.dataset.impeccableExtension` on the injected /detect.js tag, and the overlay's whole detect toggle travels over the postMessage loop that 50-scan.js installs only under EXTENSION_MODE. The visual contrast stage is not leaking either: collectBrowserFindingsAsync and scan() both consult skipScanActive(), and the offscreen path skips its visual pass on config.skipScan. The tracked live asset is regenerated (cargo xtask bundle). The oracle replays with zero unreviewed differences: the new field defaults empty and the filter is inert without it, and no CLI path sets extension mode. AI-assisted change: implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Shim test: run from a staged copy and prove the download happened The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the loader now hands off when the resume is the arrival The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live server: stop ends the process, SSE skips the mutation lane Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: the shader teardown can no longer race its own construction The new cycling assertion caught a real defect on CI: vite8-react-insert reached CYCLING with #impeccable-live-shader still painted over the page. showShaderOverlay is async. It appends its canvas synchronously, then awaits createImageBitmap and finishes the GL setup before it publishes shaderState. hideShaderOverlay returned early on a null shaderState, so a teardown that landed inside that window did nothing, and the construction then published itself over a session that had already left GENERATING, with no teardown left to run. The scroll tick kept repositioning it, which is why the CI page.html shows the canvas sized from the capture rect but styled to the cycling anchor. Every teardown now bumps a shader epoch before it does anything else, and a construction pins the epoch it owns and abandons its canvas (releasing the GL context) at every point past an await and before any publish, including both bitmap-fallback publishes. A teardown also drops a shader node that no shaderState owns, so an already-orphaned canvas cannot survive one. Reproduced by widening the append-to-publish window: with a 400ms delay after uiAppend, vite8-react-insert failed with the CI error and the probe showed the teardown arriving at CYCLING with shaderState still null. The same run passes with this change, as does a 1500ms window on insert and plain. Locally that window is about 4ms, which is why it only showed on a slower runner. The four remaining setLiveState('CYCLING') sites that did not lower the loader now do: the SSE done handler (the one route that can reach CYCLING from GENERATING), the Svelte republish remount, and the two accept failure recoveries. The e2e assertion already waits up to 5s for the shader to clear, so it was never racing a legitimate teardown; it is left as it is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: every active-session wrapper lookup goes through the resolver Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Live: a discard releases every wrapper it hid Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Release: publish the npm platform packages in one command bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * release-engine: pin checkout, upload-artifact and download-artifact at v7 The v4 pins target Node 20, which the runner now deprecates and forces onto Node 24 with a warning on every step. The rest of the workflows already use v7. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: make the temp-dir helpers unique under a coarse clock Windows' system clock is coarse enough that two parallel tests could get the same pid-plus-nanoseconds directory name and then remove each other's files (rust-windows: close_verb_round_trip_and_ownership, NotFound). A per-process counter is appended to the name. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Tests: declare the temp-dir counter in the hook cache-root tests The previous commit referenced TMP_SEQ there without defining it. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-09-04 10:42:45 -07:00
"puppeteer": "^25.1.0",
Live v2: root manifest, mount-ack protocol, AST scaffolder, mechanical accept A ground-up hardening of live mode, driven by a production session in a nested-app monorepo that hit six distinct failure classes. Full design rationale in docs/LIVE-REWRITE-PLAN.md; every Codex-reported failure now has a mechanical fix and a regression test. Roots: live/roots.mjs resolves appRoot/repoRoot/contextRoot once at boot (keyed on dev-server configs, not monorepo brand markers), persists a manifest, and every live CLI re-anchors onto it at startup, so a helper run from the wrong directory can no longer fork session state. Context files are discovered upward to the git root. Render truth: variant_mounted / variant_mount_failed events give the journal per-variant mount state; failures reach the agent's poll queue, raise a persistent error card with Retry (no more localStorage wipe), and an attach probe names root/dev-server mismatches explicitly. The browser rehydrates from the server when localStorage is gone. Svelte: the scaffolder now parses with the app's own svelte 5 compiler. Control flow survives (an each collection crosses the contract as one structured prop), keyed each blocks hydrate synthetic keys, and anything a detached preview cannot support falls back to source-preview instead of shipping a wrong scaffold. Preview modules live in per-publish revision directories, defeating stale transform caches. Accept: CSS is reconciled, not appended. Matching selectors are replaced, params bake from params.json kinds, the compiler's unused-selector pass prunes superseded rules (pre-existing dead rules protected), a selector- loss postcondition refuses any write that would drop hand-written rules, and live-complete refuses to finish while live plumbing remains in source. Also: framework registry (live/frameworks/) with a crash-safe injection journal, session-store snapshot caching with read-only reads, protocol enum consolidation, steer Send button, honest DESIGN-panel empty states. Testing: new unit suites (roots, AST scaffolder, accept CSS, accept pipeline, framework conformance); e2e now fails on preview-tree 404s, proves computed-style mount for every variant, drives the Tune panel through baked params, and injects failures (broken mounts, republish, storage loss). New runtime fixtures: monorepo-nested-vite (repo root != app root) and vite8-sveltekit-stateful (each blocks + state). Nightly full-matrix cron. An independent adversarial review pass preceded this commit; its blocker and major findings are fixed and regression-tested. This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-27 15:09:40 -07:00
"svelte": "^5",
"zod": "^4.3.6"
}
2025-11-16 14:54:35 -08:00
}