Commit Graph
164 Commits
Author SHA1 Message Date
Paul BakausandGitHub 1bcdf80f91 Fix radius var fallback detection (#687)
Strip closing var() parentheses before resolving fallback radius tokens, preserving on-scale values and actionable ignore values.

AI-assisted change: implemented with Codex under @pbakaus direction.
2026-08-31 23:27:49 -04:00
oleg kovalandGitHub f3df3ffe40 Add Veto harness support (#675)
Adds Veto detection, provider transforms, installation paths, documentation, and regression coverage.\n\nAI-assisted maintainer repair, review, and validation by Codex under maintainer direction.
2026-08-31 19:52:46 -04:00
Abdul WahabandGitHub 2cfd60765a Fix: do not flag Roboto in system font stacks (#678)
Treat the leading system face as primary so later Roboto fallbacks do not trigger overused-font, while named web-font primaries still flag.

AI-assisted merge: reviewed and executed by Codex under maintainer direction.
2026-08-31 19:20:51 -04:00
Paul Bakaus 9434dde9af Merge main: skipScan visual-contrast coverage, live overlay waivers, generated output sync
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).
2026-08-28 16:01:35 -07:00
Paul Bakaus 3818a5655b Address the Bugbot and Copilot findings on #599
- 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).
2026-08-28 14:54:04 -07:00
00095adb26 Fix: skipScan must cover the visual contrast stage too
Bugbot on PR #665: the skipScan guard emptied only the analytic
collectBrowserFindings pass, and scan()'s detached visual-contrast
stage then repopulated an ignoreFiles-waived page with contrast
markers and a second non-zero results post. Hoist the guard into
skipScanActive() and honor it in scan() and the async collector;
regenerate the browser bundle.

Adds a browser-backed regression test that reproduces the leak
(second results post carrying low-contrast findings) and pins the
zero contract; drops a tautological assert flagged in review.

AI-assisted change: implemented with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 18:38:36 +05:00
152d6940b0 Fix: harden live overlay detector waivers (#639 follow-up)
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>
2026-08-28 18:38:36 +05:00
5330fa358e Fix: honour .impeccable detector ignores in the live overlay (#639)
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>
2026-08-28 07:13:43 +05:00
5856161014 Plate pipeline, asset producer rewrite, and two detector rules for CSS standing in for material
generate-image.mjs --plate produces one raster region of the measured spec
from the comp crop, scores it against the crop, and refuses under --min.
The asset producer's job becomes producing the spec's plates. Detector
gains organic-clip-path (many-vertex polygon / curved path() clips) and
buried-raster (raster under a near-opaque wash or at near-zero opacity),
wired into both engines with fixtures.

AI-assisted (Claude).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 06:09:46 +05:00
d690349db1 Fix: keep URL basic-auth credentials on the scan origin (#657)
page.authenticate is page-wide, so a cross-origin redirect that then 401s would receive the original credentials. Attach Authorization only to requests for the scan origin.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:53:01 +05:00
d5873ff8eb Fix: redact URL userinfo from detect findings (#657)
Strip basic-auth credentials from scan-target URLs before goto and finding output, and pass them to page.authenticate instead.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:53:01 +05:00
be87f5eb86 Fix: refuse inert exact ignore-value entries (#662)
ignore-value stored exact values for rules that cannot extract one, so the entries never matched. Refuse them and point at "*" --file.

AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:43:36 +05:00
af2e8b3ac3 Fix: stream bundle downloads to disk instead of buffering
AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:43:04 +05:00
5d932f9fbe Fix: safe temp staging and downloadFile error handling (#479)
AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:43:04 +05:00
Abdul WahabandCursor daae1d4117 Fix: reject root-relative .. segments and warn per scan
Dot-segment hrefs like /../outside.css could leave the project, and a process-wide warning set hid missing-sheet notices on later detectHtml calls.

AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 19:47:54 +05:00
Abdul WahabandCursor 2b88aa5231 Fix: resolve root-relative linked stylesheets in static detect (#652)
Root-relative hrefs like /static/app.css were treated as OS-absolute and silently dropped, hiding contrast findings.

AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 19:34:55 +05:00
Abdul WahabandCursor 47e411952b Fix: own nested workspace packages and honor projectRoots first (#570)
packages/* now includes nested package.json dirs under a matched
workspace package, and Impeccable projectRoots govern a path even when
package-manager workspaces exclude it.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 08:03:16 +05:00
Abdul WahabandCursor 5d7c1cce34 Fix: inherit DESIGN.md only from a monorepo root that owns the path (#570)
findDesignRoot continued past every workspace package.json to any
workspace-declaring ancestor. It now matches the boundary against that
ancestor's globs (including negations and globstars), so excluded and
stray packages do not inherit, while included workspaces still do.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 07:47:16 +05:00
Abdul Wahab 043e8a5bfd Merge origin/main into fix/570-monorepo-design-root 2026-08-25 06:58:14 +05:00
Abdul WahabandCursor 49571365a8 Fix: rewrite Grok project hooks to the global skill path (#642)
Grok was skipped by the hook-command rewrite, so a global skill install left .grok/hooks/impeccable.json pointing at a project-relative hook.mjs that does not exist.

AI assistance: Cursor Grok 4.6 implemented this change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 04:27:01 +05:00
Abdul WahabandGitHub a735bc55cd Merge pull request #630 from pbakaus/fix/592-oklch-parseanycolor
Fix: parse oklch in visual-contrast and neon-text (#592)
2026-08-24 04:26:19 +05:00
Abdul WahabandGitHub 2e8f8dfdae Merge pull request #632 from pbakaus/fix/589-comment-strip-markup-css
Fix: strip comments in markup and stylesheets before regex matchers (#589)
2026-08-24 04:23:05 +05:00
Abdul WahabandGitHub 26bb3d3af5 Merge pull request #635 from pbakaus/fix/615-grid-1d-rails
Fix: stop flagging 1D dashed rules as grid backgrounds (#615)
2026-08-24 04:17:50 +05:00
Abdul WahabandCursor 9a7d0fbc50 Fix: skip regex literals in Astro fences and url() protocol-relative slashes
Quote-bearing regexes made the frontmatter closer miss the closing ---, and url(//…) plus interpolations were treated as SCSS line comments that hid live font-family. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 07:13:18 +05:00
Abdul WahabandCursor ba873f7599 Fix: blank preprocessor line comments inside component style blocks
Standalone SCSS/Sass/Less files already ignored // comments, but <style lang="scss"> in Astro/Vue/Svelte still scanned them as live CSS. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 07:02:44 +05:00
Abdul WahabandCursor a236137bc6 Fix: stop flagging 1D dashed rules as grid backgrounds (#615)
codex-grid-background treated any 2D px background-size as a grid, so a single hairline tiled as a dash or rail false-positived. A finding now requires two hairline gradients plus a px tile.

Prepared with AI assistance (Cursor agent), directed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:51:25 +05:00
Abdul WahabandCursor ddb609936a Fix: keep comment blanking out of script strings, preprocessor //, and Astro fences
Naive HTML/CSS comment regexes were swallowing live markup between script-string delimiters, SCSS/Sass/Less line comments still reached the matchers, and indexOf treated --- inside a frontmatter template literal as the closing fence. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:49:20 +05:00
Abdul WahabandCursor 5444031942 Fix: skip hex nested in color-mix when measuring gradient contrast (#578)
parseGradientColors treated #000 inside color-mix() as a stop, so low-contrast scored text against phantom black. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:47:29 +05:00
Abdul WahabandCursor 067665cc7e Fix: strip comments in markup and stylesheets before regex matchers (#589)
detectText only blanked comments for JS extensions, so broken-image still fired on <img> inside Astro/Vue/Svelte comments, CSS comments, and extracted style blocks. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:34:04 +05:00
Abdul WahabandCursor 1b7da15b56 Fix: parse oklch in visual-contrast and neon-text (#592)
Bare parseRgb() dropped Tailwind v4 computed colors, so contrast sampling skipped and neon-text never fired.

AI-assisted (Cursor agent).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 05:25:50 +05:00
Abdul WahabandGitHub 56f44523f7 Merge pull request #627 from pbakaus/codex/issue-624-claude-agents
Fix Claude agent installation
2026-08-22 04:59:10 +05:00
Paul Bakaus d2a9efb90f Preserve inferred agent update scope
Prepared with AI assistance from Codex under explicit maintainer authorization.
2026-08-21 09:37:53 -07:00
Paul Bakaus 16a218e632 Fix home-scoped agent freshness
Prepared with AI assistance from Codex under explicit maintainer authorization.
2026-08-21 09:23:41 -07:00
Paul Bakaus 7b94585653 Fix Claude agent installation
Prepared with AI assistance from Codex under explicit maintainer authorization.
2026-08-21 09:10:31 -07:00
Paul Bakaus 665c51b903 Fix Windows hook migration dedupe
Normalize hook command separators before matching Impeccable-owned entries so updates replace legacy Windows guards instead of duplicating them.\n\nAI assistance: Codex implemented and validated this change under maintainer authorization.
2026-08-17 06:10:16 -07:00
886cd669ef Fix: compile closest() selectors once per document in the static scanner (#575) (#595)
StaticElement.closest() handed the raw selector string to css-select's
is() on every ancestor step, recompiling the same selector N times for
an element N levels deep. StaticDocument now caches one compiled
matcher per selector (failed compiles cached as rethrowers so bad
selectors still return null). Findings are byte-identical across the
fixture corpus; scan time drops to ~62% on the fixtures and ~7x faster
on deep-DOM pages.

Prepared with AI assistance (Cursor agent), directed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 16:15:01 -07:00
Abdul WahabandCursor e975bec412 Harden monorepo design-root recognition and the home-directory stop (#570)
Read all four workspace-glob sources context.mjs reads (.impeccable
projectRoots, package.json workspaces, lerna packages, pnpm packages),
so lerna-glob roots and impeccable projectRoots no longer hit the same
abstention. Compare the walk against both the logical and realpath
forms of the home directory: on distros that symlink home paths
(/home to /var/home) the string comparison never matched, and the
post-boundary walk could inherit a workspace-declaring home's
DESIGN.md.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 01:07:52 +05:00
Abdul WahabandCursor 91f2c7b47e Fix: strip inline YAML comments when reading pnpm workspace globs (#570)
An inline comment on a pnpm-workspace.yaml packages line defeated the
end-anchored flow-list regex and the block-list state switch, so
workspaces outside apps/ or packages/ went unrecognized. Reuses the
engine's existing stripInlineYamlComment, matching context.mjs.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 23:47:59 +05:00
Abdul WahabandCursor dca8f1ca6f Fix: inherit the monorepo root's DESIGN.md in detect design-system rules (#570)
findDesignRoot stopped at the first package.json boundary, so every
design-system rule silently abstained for files inside monorepo
workspaces. The walk now continues past a workspace boundary to the
monorepo root that owns it, recognized the same way context.mjs does
(declared workspace globs, or a marker file beside apps/ or packages/
children). A nested repo with its own .git, a workspace-owned
DESIGN.md, and non-monorepo projects keep their existing behavior.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 23:41:29 +05:00
Paul BakausandGitHub a98ee8b10e Simplify local detector dispatch (#577)
* Simplify local detector dispatch

Centralize HTML-versus-text file routing for stdin, directory, and direct-file scans. Add CLI characterization coverage for both stdin paths.

Prepared with AI assistance under maintainer pbakaus's standing scheduled-refactor authorization.

* Strengthen detector dispatch characterization

Put the HTML-only finding in a linked stylesheet so the text engine cannot satisfy the static-engine assertion.

Prepared with AI assistance under maintainer pbakaus's standing scheduled-refactor authorization.
2026-08-14 05:45:42 -07:00
Paul BakausandGitHub 628509b948 Merge pull request #553 from pbakaus/fix/issue-547-shadow-token-context
Allow documented sidecar shadow colors in shadow contexts (#547)
2026-08-13 17:06:00 -04:00
Paul BakausandGitHub fde4a3ee71 Merge pull request #554 from pbakaus/fix/548-layout-transition-quoted-values
Fix: layout-transition false positives on JSX quoted transition values (#548)
2026-08-13 17:01:23 -04:00
Paul BakausandGitHub e36833ce21 Merge pull request #521 from digitallamb/pr/hermes-provider
Add Hermes Agent as a supported provider
2026-08-12 16:07:43 -04:00
ac0416b655 Stop assuming white when a background cannot be read (#541)
* Stop assuming white when a background cannot be read

Dark themes came back from a scan buried in low-contrast findings that
all claimed the light text sat on #ffffff. Two live runs against
impeccable.style produced 102 and 95 of them.

Two causes, both fixed here.

Parsing. Browsers keep the authored color space in getComputedStyle
output: oklch() stayed oklch, but color-mix results come back as
color(srgb 1.04 0.72 -0.21), wide-gamut authors get color(display-p3
...), and lch()/lab() survive verbatim. The parser read none of those, so
those surfaces registered as unset. parseGradientColors was worse: it
matched only rgba() and #hex, so a ground painted as
linear-gradient(oklch(...), oklch(...)) counted as a gradient with no
stops at all.

Guessing. When the ancestor walk ran out of readable color it returned
white, and on a body-level gradient it returned white without even
looking. Light copy on a lacquer-black page then measured 1.3:1 against a
canvas the visitor never sees.

resolveBackgroundInfo now separates three outcomes: a resolved surface, a
gradient the caller should fall back to stops for, and an unreadable
layer. The last one makes both color adapters skip their contrast checks
entirely. White survives in exactly one case, the one that earns it:
every layer up to the document root was genuinely transparent.

Color conversions moved to cli/engine/shared/color.mjs and gained lab,
lch, and color() for srgb, srgb-linear, and display-p3. Spaces outside
that set return null, which now routes to abstention rather than to a
color nobody painted. Every conversion is pinned against what Chrome
itself paints for the same string.

Rescanning impeccable.style: 102 low-contrast findings down to 30, none
of them on an invented white ground.

Assisted-by: Claude Code

* fix: address PR review bot findings on background resolution

- Treat a url() image layer stacked above a gradient as an occluding,
  unreadable surface: resolveBackgroundInfo now returns unresolved so the
  gradient-stop fallback never measures stops the image hides
  (greptile-apps finding, reproduced in Chrome).
- Route the glow and AI-palette DOM adapters through resolveBackgroundInfo
  so an unresolved surface makes them abstain instead of hunting gradient
  ancestors past an unreadable layer (Cursor Bugbot finding).
- Resolve background-color keywords jsdom hands through verbatim:
  inherit now reads as no-paint (the ancestor walk IS its resolution) and
  currentcolor substitutes the element's own computed text color instead
  of forcing an abstention (Copilot finding).
- Regression coverage in the dark-theme fixture for all three, asserted in
  both the jsdom and real-Chrome suites; browser detector regenerated.

AI-assisted: prepared with Claude Code at the maintainer's direction.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep zero-offset glow findings when the surface is unreadable

The browser glow adapter abstained from the whole element when
resolveBackgroundInfo reported an unreadable surface, which also dropped
zero-offset chromatic halo findings that do not depend on the background
at all. It now skips only the gradient hunt past the unreadable layer and
scores the halo tell against a null surface, matching what the static
loop already did. Fixture cases pin both sides: the halo over a url()
image ancestor flags in both engines, and an offset chromatic shadow on
the same unknown surface stays abstained.

Also hardens the currentcolor background substitution with the
parseColorResolved fallback used by the text-color path, and adds fixture
coverage proving tokenized currentcolor surfaces already resolve through
the static cascade (flag when knowable, abstain when the token is
undefined).

Addresses Cursor Bugbot review findings on PR #541.

AI-assisted-by: Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: abstain on translucent gradients over images, drop phantom color-mix stops

Two follow-up review findings on the merge with main.

A gradient leading a url() layer was treated as a resolvable surface even
when its stops are translucent, so the glow and AI-palette hunts averaged
wash stops (a 20% black wash reads as pure black) while the real surface
blends with image pixels the engine cannot read. resolveBackgroundInfo now
marks gradient-over-image unresolved unless every readable stop of the
leading gradient is opaque, in which case the gradient provably covers the
image and remains the scorable surface.

parseGradientColorsModern predated this branch's parseGradientColors
rewrite: its second regex pass re-extracted color tokens nested inside
color-mix() stops that the shared parser already captures whole via
balanced-paren tokens, appending ingredient colors that are never painted.
The worst-case stop ratio then invented low-contrast findings against a
color nobody sees. The helper is removed; all callers use the shared
parser, which covers the modern syntaxes it existed for.

Fixture coverage pins both: the translucent-wash-over-image glow abstains
in both engines, an opaque gradient over an image still flags in the
browser, and the color-mix wash case stays clean in the static engine.
Each new assertion was verified to fail against the previous engine.

Addresses Greptile and Cursor Bugbot review findings on PR #541.

AI-assisted-by: Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-11 14:59:07 -04:00
digitallamb def69e157b fix(cli): use imported resolve/sep in hermesGlobalHome (#521)
The function called `path.resolve` and `path.sep` but only named-
imports `resolve` and `sep` from `node:path`. The ReferenceError was
swallowed by the try/catch, so $HERMES_HOME was silently ignored and
profile-scoped installs always landed in ~/.hermes instead of the
active profile. Greptile (P1) and Cursor Bugbot (High) flagged this
on 2026-08-10. Adds 6 regression tests covering default, default-
profile, active-profile, cross-home leakage, the override map
integration, and the full e2e pipeline. Verified by reverting the
fix and observing the relevant tests fail.
2026-08-10 22:47:56 -07:00
aee5ddd10c data-impeccable-ignore scoped waivers + occlusion and image-backed contrast FP fixes (#559)
* Add data-impeccable-ignore scoped waivers; fix occlusion and image-backed contrast FPs

Three changes that let a page hosting deliberate anti-pattern exhibits
scan clean without losing coverage, prepared with AI assistance (Claude
Code) on maintainer instruction:

- data-impeccable-ignore="rule-a rule-b" (or "*" / bare) on any element
  suppresses matching findings for its whole subtree, in the browser
  overlay, the extension, and the static engine. The DOM twin of the
  line-based impeccable-disable comments (which a live DOM cannot
  apply) and the generalization of data-impeccable-allow-kickers.
  Applied at the addBrowserFindings choke point, at the static element
  walk, and for regex findings that carry a live selector.

- text-occlusion: an occluder whose effective opacity multiplies out to
  ~0 paints nothing. An opacity-0 range scrubber stretched over a
  before/after comparison produced 16 "100% covered by an opaque
  element" findings on one page because elementFromPoint returns it and
  its UA background-color read as opaque paint. Invisible-at-rest
  elements are also no longer probed as victims.

- Analytic contrast now skips what it cannot measure: a url() image
  layer anywhere in the background stack ends the gradient-stops walk
  (dark ink on a bright gold-leaf image measured 2.6:1 against the wash
  composited over the wrong base), and elements that are invisible at
  rest (visibility hidden, effective opacity ~0 — hidden scene decks)
  are skipped by the color checks in both engines. The static cascade
  now tracks opacity to support this.

Covered by a new scoped-ignore fixture (exact rule, star, comma list,
nested depth, wrong-rule control) tested in both engines, a scrubber
pass case in the occlusion fixture, and image-backed / photo-panel /
hidden-scene pass cases in the gradient-ground fixture. Full suite
passes; browser and extension bundles regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CSS-scan findings carry their enclosing selector; browser pass resolves them

Page-level CSS-text findings (marquee, dark-glow, radial-halo,
repeating-stripes, codex-grid, ai-color-palette, image-hover-transform,
pseudo/inset side-tab stripes) now attach the selector of the rule that
matched, via a best-effort enclosingCssSelector() helper or the
selector already in scope. The browser pass resolves that selector
against the live DOM: pseudo segments are stripped, a selector that
renders nowhere on the page drops the finding (the CSS ships there but
the pattern never paints — the live DOM is ground truth in a browser
scan), and matches under a data-impeccable-ignore ancestor are waived.
Static scans are unchanged: partial documents keep the text-level
findings. Applied with AI assistance (Claude Code).

Covered in the scoped-ignore fixture: a live marquee under a marquee
waiver is suppressed, and dead two-axis grid CSS matching no element is
dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Attribute selectors on gradient-text and bounce-easing page emitters too

Same mechanism as the previous commit, extended to the three page-level
motion/text emitters that were still selector-less. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* text-overflow: skip SVG content; scrollWidth lies there

Chrome reports arbitrary non-zero scrollWidth/clientWidth on SVG
elements (a <text> gave 78/48 while its rendered length sat inside its
box), so the box-metric delta is noise. SVG clips to its own viewport
anyway. Pass case added to the quality fixture. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Overlay samples image-backed text at the pixel level by default

Visual contrast gains a third mode. Explicit true still runs the full
sampled pass, explicit false still disables everything (the mode the
test suites use), and unset — the default overlay run — now samples
ONLY image-backed text: the one class the analytic walk deliberately
skips, because a url() layer's pixels are unknowable without looking.

The cost is bounded and the method is precise: at most a 3x3 grid of
sample points per candidate (degrading to 3 or 1 for small rects), the
source image drawn once to a canvas with only those pixels read, and
glyph ink never pollutes the samples because the image is drawn alone.
A cross-origin image without CORS headers reports unresolved rather
than guessing. Applied with AI assistance (Claude Code).

Covered by a new fixture: white text on a near-white same-origin
data-URI image background flags via sampled pixels under default
options; dark ink on the same image passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: root opacity, keyframe steps, static parity, attributed fixtures

Applied with AI assistance (Claude Code), addressing all seven findings
from the automated reviews:

- effectiveOpacityDOM walks through body and html: a page-fade wrapper
  with body/html opacity 0 hides every descendant (Greptile executed a
  Chromium repro of the false positive).
- enclosingCssSelector refuses `from`/`to` keyframe steps, which read
  as never-matching type selectors and got valid findings wrongly
  dropped by the zero-match rule (Bugbot, high). Regression case: an
  overshoot bezier inside a `to` step must survive as page-level.
- The static cascade now inherits visibility, so descendants of a
  hidden container compute as hidden like the browser path; a declared
  visibility:visible still overrides.
- The static engine applies scoped waivers to selector-backed
  html-pattern findings, mirroring the browser — but keeps findings
  whose selector matches nothing, since static scans see partial
  documents.
- The scoped-ignore fixture grows to the mandated matrix: 4 flag cases
  (control, other-rule waiver, sibling waiver, misspelled rule id) and
  5 waived shapes (exact rule, nested depth, star, comma list, self),
  each with a unique border width so every finding attributes to
  exactly one case in both engines' tests.
- The image-backed contrast test pins its cases via the sampled
  finding's candidate text: the white-on-light specimen must flag and
  the dark-ink control must stay clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: image-only starvation, selector rejection class, inset stripes

Second review round, applied with AI assistance (Claude Code):

- The image-only filter moves inside the candidate collector, before the
  cap: gradient/opacity/filter candidates earlier in DOM order no longer
  consume the 12-candidate budget and starve the url()-backed texts the
  mode exists to sample (Bugbot, high). The regression fixture packs 14
  gradient decoys ahead of the photo panels, and the test now drives the
  overlay entry (impeccableDetectAsync, default options) rather than
  detectUrl's Node-side full fallback, which is where the image-only
  mode actually lives.
- enclosingCssSelector no longer rejects the child combinator or quoted
  attribute selectors; only braces and angle brackets disqualify.
- The inset box-shadow side-tab scanner attaches its selector like the
  pseudo-element scanner does, so those findings waive and dead-drop
  the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:31:46 -07:00
5f7b001cbe Fix: measure gradient body grounds instead of assuming white (low-contrast false positives) (#557)
* Fix: measure gradient body grounds instead of assuming white (browser mode)

A page whose ground is set via background: linear-gradient(...) on body
leaves backgroundColor transparent, and resolveBackground assumed white
for any body/html-level gradient. In a real browser that assumption is
wrong: the shorthand is always decomposed there, so reaching that branch
means the ground truly is the gradient. On a dark oklch gradient ground
(impeccable.style's lacquer) this turned every light-on-dark text into a
~1.3:1 "on #ffffff" low-contrast finding, ~120 false positives on one
site. Browser mode now returns null so the caller measures against the
actual gradient stops; the white assumption stays for jsdom, where the
undecomposed-shorthand rationale still holds.

Gradient stops also now parse modern color syntax: computed
backgroundImage keeps oklch()/oklab()/hsl()/hwb() stops as authored, and
parseGradientColors only read rgb()/hex, so a token-driven gradient
ground was invisible even once the walk deferred to it. New
parseGradientColorsModern routes those stops through parseAnyColor.

Covered by a Puppeteer fixture (dark oklch body gradient): light text on
the ground must not flag, muted dark-gray ink must, proving the stops
are measured rather than the checks silently skipping.

Prepared with AI assistance (Claude Code), on maintainer instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Composite translucent layers over gradient stops; parse modern glow stops

Review fixes from PR #557's automated reviews, applied with AI
assistance (Claude Code):

- Cursor Bugbot found the new browser-mode early return discarded the
  translucent ancestors resolveBackground had collected: text on a
  frosted wash over a body gradient was measured against raw stops.
  resolveGradientStops now collects translucent layers during its own
  walk (through readCascadeBackgroundColor, extracted so both walks
  read surfaces identically) and composites every stop under them.
- Copilot flagged the other legacy parseGradientColors call sites. The
  glow-context fallback now uses parseGradientColorsModern, since body
  gradients reach it more often after this change. The AI-palette rule
  and the injected analytic sampler stay on the legacy parser
  deliberately: the former is a rule-behavior expansion deserving its
  own fixtures, the latter degrades to pixel sampling or a skip.
- Greptile asked for standard fixture structure: the fixture now has
  labeled flag/pass cases (3 flag, 5 pass) including the frosted-wash
  pair that locks the overlay compositing in both directions and a
  legacy hex-stop gradient guarding the original parser path.

The test scopes itself to the DOM path via visualContrast: false, the
suite's established pattern; the screenshot sampler is a separate
subsystem with its own coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Pin gradient-ground flag cases to their snippet signatures

Bugbot follow-up: a count-only assertion let an offsetting miss and
false positive cancel, especially the frosted pair. Each flag case now
asserts its full text-on-background signature, so the frosted case must
measure against the composited wash and the count guard excludes any
pass case flagging in its place. Applied with AI assistance (Claude
Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Comments: the static path is the custom engine now, not jsdom

jsdom left the dependency tree when the static-html engine (StaticElement
+ css-cascade.mjs) replaced it, and that engine does decompose the
background shorthand, so the comments this PR added were dated in both
name and rationale. Only comments touched by this PR are renamed; the
~40 legacy jsdom mentions elsewhere in checks.mjs are a separate sweep.
Applied with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Static engine: measure body gradients too, dropping the white assumption

Follow-up to the browser-mode fix: the white assumption for body/html
gradients was a jsdom guard, and jsdom is gone. The static cascade
decomposes the background shorthand (expandStaticDeclaration) and
preserves var() colors for later resolution, so a missing solid under a
body gradient is now as real in static mode as in a browser — and the
static engine had the identical false-positive class (light text on a
dark gradient ground flagged "on #ffffff") while missing the muted-ink
true positives on the same page.

The old catastrophic case cannot recur: opaque stops fully cover any
hidden solid (they are the ground), alpha stops composite over the
resolved base or the white canvas default, and unresolvable stops drop
rather than guess.

Static twin of the browser test added over the same fixture; the full
suite, the url()-ancestor guard, and a source scan of impeccable.style
(0 low-contrast findings) all stay clean. Applied with AI assistance
(Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:35:55 -07:00
Abdul WahabandCursor d23fa1c882 Fix: layout-transition false positives on JSX quoted transition values (#548)
The value-capture regex stopped only at ;{}, so in single-line JSX
style objects it ran past the closing quote and swallowed later
properties, flagging layout props that were never transitioned. The
capture now stops at the matching closing quote when the value is a
quoted string, falling back to the old bounds for real CSS.

Prepared with AI assistance under maintainer direction.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 14:00:55 +05:00
Abdul WahabandCursor 520a55547e Admit one brace level inside shadow interpolations
Review finding on #553: an object-literal argument like
${getOffset({ size: 2 })} ended the interpolation match at the inner
closing brace, losing the shadow context. Interpolations now admit one
level of braces (with paired quotes inside); the shared subpattern is
hoisted into compiled constants. Deeper nesting stays fail-safe by
design: a line-scoped regex cannot balance arbitrary braces, and the
miss produces a waivable finding, never a leak.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 13:28:11 +05:00
Abdul WahabandCursor 82234515e0 Admit paired quoted strings inside shadow interpolations
Review finding on #553: the interpolation subpattern excluded quotes,
so a documented shadow color after ${getShadow('lg')} or a quoted
ternary branch lost its context and fired as drift. Interpolations now
admit complete single/double-quoted strings; the quotes pair up inside
the ${...}, so an unpaired quote or the template's closing backtick
still ends the context and the allowance cannot leak to a later
property.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 13:18:10 +05:00