The generated browser bundle is rebuilt from the merged engine sources in the next commit's build step (both branches had regenerated it).
AI-assisted (Claude Code).
Closes the residual page-inference edge (no recorded artifact, no index.html, several root HTML files) by making the conservative refusal self-correcting instead of adding more inference; the gate never falsely passes in that configuration, only asks for the page.
AI-assisted (Claude Code).
Greptile's fourth finding on the seam: the no-artifact path was depth-limited. The page default (index.html, or the one .html at the root) now applies before unreferencedPlates, so the link-following path, which is exact and unbounded, handles every build that has a page; the bounded walk is only the no-page fallback.
AI-assisted (Claude Code).
Bugbot on #599: path.resolve treated /assets/hero.css as filesystem-absolute. Both the working directory and the artifact's directory are tried; unreadable candidates skip.
AI-assisted (Claude Code).
Greptile's third P1 on the same seam: a stylesheet linked from the artifact but outside the bounded walk's root, depth, or file limit was still invisible. The hrefs the artifact itself declares are resolved against its directory and joined to the corpus, which closes every variant.
AI-assisted (Claude Code).
Greptile's follow-up P1 on #599: with --artifact set, only that HTML file was read, so a plate referenced exclusively from a linked stylesheet still read as unused. The bounded source walk now runs either way.
AI-assisted (Claude Code).
Greptile P1 on #599: unreferencedPlates read a plate referenced only from assets/hero.css as unused and the hero gate refused a valid build. The extension filter already keeps binaries out of the walk.
AI-assisted (Claude Code).
- keyChroma re-encodes with the PNG's tEXt chunks intact (the embedded prompt survived generation but not keying)
- organic-clip-path counts relative curve commands too (path data letters are only commands, so the match is case-insensitive)
- buried-raster normalizes percentage alphas (parseFloat('80%') read as 80) and reads 4- and 8-digit hex alpha instead of treating #rrggbbaa as opaque
- the extension-injected-node skip in checkQuality runs before any finding is pushed (a low-opacity injected raster was recorded, then returned by the skip)
- fake-mode plates carry impeccable:fake tEXt and the plates gate's crop-identity refusal skips them (fake mode IS the crop by design; the refusal is for models shipping the comp's pixels as artwork)
Findings by cursor[bot] and Copilot on PR #599; detector engines rebuilt (build:browser, build:extension).
AI-assisted (Claude Code).
CI resolves playwright but has no downloaded chromium; launch threw instead of falling back to the catalog ranking, and every spec gate downstream failed. Launch failures now return the no-browser path (and the browser test skips instead of asserting).
AI-assisted (Claude Code).
Paul's decision on the tenth sweep's design question: hard vetoes (missing region, contradicted plate or text, SVG illustration, clipped plate, invented ink) stay unconditional; at overall >= HERO_MIN the numeric readings (ink colour, letter-spacing, line pitch, strip heights, box positions) print as advisories with the pass and belong to the polish pass. Every sweep-10 sample closes its hero under this condition, which settles 07 without another paid round.
Ninth-sweep defects: codeDrawn / container / bleed now persist into spec.json with WARN lines (an overridden refusal used to vanish from the record); font-match probes os.tmpdir() and points TMPDIR at .impeccable/tmp when the sandbox /tmp is unwritable (every ninth-sweep rank silently fell back to the catalog).
AI-assisted (Claude Code).
serve-question answered POST /build-path with 200 and only then wrote the
flip file. The caller is a separate process, so the response could reach it
while the server was still preempted before the write landed: a poller that
trusted the 200 could look for the flip file and miss it.
Measured on a loaded machine, the old order lost that race 29 times out of
40; writing first and answering after loses it 0 times out of 40. This is
what made tests/serve-question.test.mjs fail intermittently in CI on the
Node 22 job while passing on Node 24.
AI-assisted change: diagnosed and implemented with Claude Code under
maintainer direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read waiver config from every live root (appRoot, contextRoot, repoRoot),
so monorepo projects whose config lives at the repo root reach the
overlay; serialize served roots and page identities repo-relative there.
Resolve each page URL to its actual serving file via the inject config's
resolved page list before applying file-scoped waivers; ambiguous URLs
keep the conservative common-ancestor fallback (PR #645 review
discussion r3840011436).
Honour detector.ignoreFiles: a wholly waived page now scans to zero
findings in the overlay, matching the CLI and the edit hook.
Guard the resolver call so a throwing resolver degrades to an unfiltered
scan instead of breaking the detect toggle.
Match design-system-color waivers by color value across hex and rgb()
spellings, and stop extracting font values for bounce-easing findings,
mirroring extractFindingIgnoreValue. Regenerate the browser bundle.
AI-assisted change: reviewed, planned, and implemented with Claude Code
under maintainer direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The per-project state dir key is now the readable separator-mapped
slug plus an 8-hex sha256 of the resolved project path. The readable
part alone is lossy (/x/my.app and /x/my-app both mapped to -x-my-app
and shared hook state); the digest keeps distinct projects' cache and
pending state apart while the dir name stays human-scannable.
- Tilde roots now expand via os.homedir() instead of HOME/USERPROFILE
with a '.' fallback. When no home dir can be determined, expansion is
rejected and state falls back to the project-local default rather
than anchoring under the hook process's working directory.
- Tests updated to the digest-suffixed slug via a mirrored slugFor()
helper, plus two new cases: colliding readable slugs get distinct
state dirs, and the tilde form resolves identically to the explicit
homedir-joined form.
Prepared with AI assistance (Claude Code) under direction of
0xDarkMatter, per the maintainer-approved issue #422.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Env files and settings JSON hand '~/caches' to Node unexpanded; without
this it would resolve to a literal '~' directory under the process cwd.
Mirrors the exact treatment IMPECCABLE_HOOK_LOG already gets in
writeAuditLog (HOME || USERPROFILE fallback), plus the Windows '~\'
spelling.
Prepared with AI assistance (Claude Code) under direction of
0xDarkMatter, per the maintainer-approved issue #422.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- hookStateDir now trims the env value (stray whitespace in env files)
and path.resolve()s both the root and the cwd, so trailing separators
and relative segments slug to the same per-project dir.
- The #344/#305 persist gate also treats an existing (possibly
redirected) cache file as the opted-in marker. Without this, once
state relocated, clean-edit editCount bumps stopped persisting because
the project-local .impeccable/ dir never appears. No-op under stock
paths, where the cache file lives inside .impeccable/.
- New tests: slug normalization equivalences, whitespace trim, graceful
persistCache failure on an unusable root, and three runHook
end-to-end cases (findings persist + dedup through the redirect,
clean-edit editCount persistence, and the no-footprint no-op gate
holding under redirect).
Prepared with AI assistance (Claude Code) under direction of
0xDarkMatter, per the maintainer-approved issue #422.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Honor an optional IMPECCABLE_CACHE_ROOT env var in getCachePath() /
getPendingPath(): when set, hook.cache.json and hook.pending.json land
under $IMPECCABLE_CACHE_ROOT/<project-slug>/ (slug = project path with
[:\/.] mapped to hyphens, mirroring Claude Code's ~/.claude/projects/
convention). Unset or blank env keeps stock project-local behavior.
User-authored config (config.json, config.local.json, design.json)
deliberately stays project-local - only disposable state relocates.
Also clears ambient IMPECCABLE_CACHE_ROOT at the top of hook.test.mjs so
a developer running the suite with the redirect active still gets
deterministic stock-path assertions; the new suite sets and restores the
var explicitly.
Prepared with AI assistance (Claude Code) under direction of
0xDarkMatter, per the maintainer-approved issue #422.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cursor's review caught the previous commit over-correcting. One tree listed
at two depths (prototype/*.html plus prototype/library/**/*.html) derived
two roots, and requiring a waiver to match under both stopped a normal
project-relative waiver like prototype/index.html from applying anywhere.
The rule both reviews were circling is simpler: one live session is served
by one server, so a single document root must sit at or above every
configured page. The only prefix the resolver can safely assert is the
deepest common ancestor of the glob roots. Nested roots collapse to their
shared tree, so normal waivers keep applying. Disjoint roots (src/ and
public/) share nothing, so no prefix is asserted and only the URL path
itself matches, which keeps the earlier fix intact: a src/foo.html waiver
still cannot hide a finding on a page served from public/foo.html.
This also deletes the match-under-every-root machinery from the previous
commit; with a single asserted prefix, plain matching is enough.
Also switches the new test file to derive the repo root from
import.meta.url rather than process.cwd(), per review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile's review found a real bug in the new resolver. When the live config
lists pages under more than one folder (src/**/*.html and public/**/*.html),
the overlay treated a URL like /foo.html as src/foo.html and public/foo.html
at the same time. A waiver written only for src/foo.html could then hide a
finding on the page actually served from public/foo.html. That fails in the
worst direction: a real finding disappears and nothing says so.
The overlay can never look up the right file. The live server does not serve
the pages; the project's own dev or static server does, and its URL-to-file
mapping is invisible from here. So the fix stops guessing: a file-scoped
waiver now applies only when it matches the URL path itself, which is true
whichever folder serves the page, or when it matches under every configured
folder, so no possible reading disagrees. Anything ambiguous shows the
finding, which is also what the CLI reports for the file really being served.
With a single configured root, the common case, nothing changes: the new
rule reduces to the old behaviour exactly. Multi-root projects keep three
ways to write a waiver that still applies: name the file under each folder,
use the bare path, or use **/.
Two new unit tests pin the ambiguous case and the safe spellings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live overlay's detect scan ran unfiltered: requestDetectScan() posted
only { scanId }, so detector.ignoreRules and detector.ignoreValues in
.impeccable/config.json reached impeccable detect and the edit hook but
never the surface a designer actually watches.
The server now serializes the project's detector waivers into the /live.js
prelude (window.__IMPECCABLE_PROJECT_IGNORES__), read per request through
hook-lib's readConfig so config.local.json wins and edits land on the next
tab reload. A new script part, live-browser-ignores.js, resolves that
config against the page URL when a scan starts: ignoreRules suppress
outright, wildcard ignoreValues suppress their rule in the files their
globs name, and the remaining entries ride along as disabledValues for the
detector to match on each finding's own value. The detector bundle applies
those where the findings are assembled, since the overlay draws its own
markers from the collected findings.
Scope resolution mirrors cli/lib/impeccable-config.mjs deliberately: the
same glob dialect (globToRegex, including {a,b} alternation), the same
path-suffix matching as findingMatchesScopedIgnoreFile, and the same
refusal to apply an unscoped wildcard entry. The served-root prefixes that
bridge project-relative globs and site-relative URLs come from the inject
config's own files globs, never from the ignore globs; deriving them from
the ignore globs lets one entry scoped to prototype/library/** lend its
prefix to every page and suppress site-wide, which looks like success
because the numbers go down.
Known gaps, recorded in the detector comment: the motion value extractor
is not mirrored, so a value-scoped bounce-easing waiver only matches when
the finding carries ignoreValue directly, and design-system-color matches
on the normalized string without the CLI's color-equality fallback.
Tests: unit tests for the resolver part (stale globals, string ignoreRules,
malformed entries, directory URLs, percent-escapes, glob metacharacters,
the roots trap), an extension-mode puppeteer test that disabledValues
suppress exactly the waived findings, and the live-browser regression pin
now asserts the new scan config shape instead of { scanId }.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sol build named its two carburetor drawings 'countable ... geometry' chrome regions, drew them in inline SVG, and the SVG ban never ran because state.artifact was null.
AI-assisted (Claude Code).
A reference, not the page: --r-<id>-x/y/w/h in % of the comp (plus cap height, font-size, family, weight where measured) to bind to any markup, and hero-reference.html with every region at its box and every plate placed with object-fit: contain, as a check on positions. Attacks the most common execution failure of weaker builders (badly positioned, overflowing, pushed below the fold) without dictating structure to strong ones; overlapping boxes are overlapping boxes and the gate reads pixels regardless.
AI-assisted (Claude Code).
From the human review's most repeated pin ('terrible svg instead of asset', on every model) and from sessions that wrote 'ship' with the hero open. Icons, arrows, chevrons, and runtime data charts stay code; diagrams, notation, and leader lines are plates.
AI-assisted (Claude Code).
From the final review batch: 'bad asset crop (crops are never allowed)' twice, 'letter spacing way too wide'. A crop resampled to the region scores 99.8% structure against the raw region; a produced plate scores 30-60.
AI-assisted (Claude Code).
A build reached hero 81% and stalled on eight staff rows read one at a time (and both ways on ink colour). The session asked the user and was told to build as written; the force was legitimate and recorded.
AI-assisted (Claude Code).
A session's own note said its hero sat at 67 because the 10x10 grid boxes straddled two elements each, and it was right: every downstream measurement (cap height, line count, structure) inherited the slop. Text and control regions now snap to the dominant connected ink inside the span (page-ground threshold, dilated cells, masses touching the span's sides lose to inside masses), keep the span on the record for coverage, and can opt out with snap: false.
AI-assisted (Claude Code).
The shifted copy's padding read as a white 'letterbox' on the build in every human review. A vertical spine came back white on red where the comp had black in five builds; its ink colour is now compared even though its type cannot be measured.
AI-assisted (Claude Code).
From the second review batch: the best build of the fifth sweep passed the hero at 87% with the cover arch cut flat on the left (object-fit: cover on a box narrower than the shape), and legends, badges, and extra controls one or two cells wide slipped under the invented-ink floor. comp-spec measures the artwork's contiguous contact with each box edge against the page ground; the spec gate refuses such a box unless bleed is set. Human pass line landed at comp-diff 72-73; HERO_MIN stays 0.72.
AI-assisted (Claude Code).
From the first human review of sweep-3 builds (pins on 12 samples): fonts at the wrong size, weight, colour, or place; nav bars too tall; kickers and dividers the comp does not have; a footer strip pushed off the frame that read as drift on ground colour alone; a drawing filed as chrome with no note to catch it.
- lib/hero-checks.mjs: textRegionCheck (cap height, line count, ink density, ink colour, first-line offset vs the comp crop, measured fresh), chromeStripCheck (first rule row), inventedInk (build energy over a calm comp cell and neighbourhood). Wired into gateHero as reasons; invented ink vetoes at 4% of cells.
- comp-diff verdictFor: detailRaw < 0.15 is missing whatever the palette.
- comp-spec: every region carries a note.
- font-fingerprint: the tall-line filter takes its median over lines with real mass, so two display lines above a small line are not 'tall'.
AI-assisted (Claude Code).
A session named seven regions for a page with three plates, a table, a note, callouts and a spine, so the hero gate could name nothing and the score sat at 70. A code region is one element; a column is a container of several.
AI-assisted (Claude Code).
Staff rules and a black page edge fused eight track rows into one 389px 'line'. A session forced two gates by quoting a brief line ('should feel like an extension of her artwork') as permission; a force now needs the user's words reported or quoted, a downgrade verb, and the comp noun in one reason.
AI-assisted (Claude Code).
A comp region drawn on the 10x10 grid over-covers: a body-copy crop carries the last headline line above it and a drawing beside it, and one session measured 'thread-body' at cap 160px off a carburetor drawing and ranked Londrina Shadow for it. Tall non-text 'lines' leave the mass reference; lines cluster by cap height and the cluster holding the most ink (multi-line first) is measured, re-applied after upsampling.
The plates NEXT line now names generate-image.mjs --plate as the tool (harness image tool only as fallback) after a session spent 25 turns keying plates with magick.
AI-assisted (Claude Code).
font-match / catalog index
- Index schema 2 adds a third render, 48c (48px cap, ALL CAPS text). Caps headline crops have no x-height band and ranked against mixed-case renders as barcode faces; they now route to the caps render.
- Non-text families (barcodes, redacted, flow, dingbats, effect faces) are excluded from candidates.
- The distance adds a gross width and weight gap (log ratio of advance and ink density) so a face 50% wider or 35% lighter cannot rank first on run-length detail alone; the index stores those readings.
- Multi-line all-caps crops vote on x-height across lines: one line's crossbars no longer give the crop a spurious x band.
- With no browser, --rank records the catalog's nearest face (source catalog, size estimated) so the spec gate can close; the NEXT line and new-work.md say not to install a browser or hand-write a choice.
- font-match stamps the choice it writes; the spec gate refuses a chosen face it did not write (sessions typed Arial Narrow into spec.json to pass).
- IMPECCABLE_NODE_MODULES lets a harness lend a playwright.
comp-spec
- A region note that describes painted material (diagram, drawing, photo, texture...) under a code kind is refused at the spec unless codeDrawn is set: the exploded carburetor filed as chrome is a plate about to be redrawn in SVG.
build-phase
- The control ink-box veto applies only when the comp's ink is a discrete element and the build's box is too; a full-width bar told one session six times that 1376x87 was 1382x102 with no edit able to move it.
AI-assisted (Claude Code).
comp-spec, comp-diff, build-phase, font-match, and generate-image now decode any comp raster via loadRaster(), converting non-PNG input to <file>.png next to the source. Sessions used to hit 'not a PNG' and rewrite the .webp in place with PNG bytes, which broke transcript replay (a later step rewrites the comp beyond the cut) and left a mislabeled file.
AI-assisted (Claude Code).
lib/font-fingerprint.mjs replaces the three-number fingerprint with
size-invariant shape features (x-height ratio, stroke contrast, stem width,
run-length quantiles, roundness, serif signal, width spread) and a
noise-normalized distance; family recall on a held-out self-test rose from
13% to 72% top-5. data/font-index.json carries the whole Google Fonts
catalog (3,092 faces at two cap sizes, 707 KB); font-match --rank fingerprints
the comp crop, takes the 25 nearest faces from the index (plus the model's
own names), renders them at the comp's cap height, ranks by the same
distance, and prints a proof sheet and the CSS to use. scripts/build-font-
index.mjs rebuilds the index at release time.
AI-assisted (Claude).
Co-Authored-By: Claude <noreply@anthropic.com>
The first paid confirmation sweep showed the failure: models rendered the
three comps first and ran build-phase.mjs start after, so a session cut at
the composition pick carried no state.json and the resumed model followed
the conversation ('translate the comp into HTML now') instead of the
phases. Decision comps (.impeccable/mocks/decision/) are unaffected;
--force-mock overrides.
AI-assisted (Claude).
Co-Authored-By: Claude <noreply@anthropic.com>
The first paid confirmation run hit a TypeError in the ink-box check
(report regions carry normalized w/h at the top level, not under box);
runGate now catches a throwing gate and returns a one-line refusal with
an explicit force path so the run is not lost to a tool bug.
AI-assisted (Claude).
Co-Authored-By: Claude <noreply@anthropic.com>
font-match.mjs fingerprints a text region's lettering in the comp (cap
height, glyph advance per cap = width class, ink fraction = weight class,
tracking), renders candidate faces at that cap height in a headless
browser (yours plus a shortlist for the width class), and ranks them by
distance with a proof sheet; the spec gate refuses to close until the lead
text region is measured and ranked. generate-image --plate keys ink-on-
ground plates to alpha (chroma) so the drawing sits on the page's own
ground; the plates gate scores keyed plates composited over the region's
ground. comp-diff reports each region's ink box; the hero gate names a
control whose box height or width differs from the comp. The hero
instruction copies the comp's words verbatim; rewording is a stated
decision after the hero passes.
Driven by a human review of the r3 side-by-sides: face width and weight,
plate ground, control row height, and content substitution.
AI-assisted (Claude).
Co-Authored-By: Claude <noreply@anthropic.com>
plateVerdict() is shared by the plates gate and generate-image's PLATE-WARN
so they cannot disagree; --plate picks a frame that clears the 1.5x width
floor (a square region wider than 682px takes the 1536 landscape frame);
'missing' on text/chrome/control regions requires the build region to be
near-empty, so a 12px rule a few pixels off reads as contradicted or drift,
not missing; the responsive gate does not re-litigate a plate that passed
the hero; record hero after close does not inflate the attempt count.
AI-assisted (Claude).
Co-Authored-By: Claude <noreply@anthropic.com>