fix(cli): contrast gate judges only readable content; examples pass check

Three filters keep the escalated contrast gate honest, each surfaced by
running check across the registry examples:
- data-layout-ignore (the layout audit's existing decorative opt-out)
  now also excludes set-dressing text from the contrast audit — one
  vocabulary for 'not copy a viewer must read'.
- Text that has (nearly) left the canvas is skipped: sampling a clamped
  off-canvas box reads border pixels and produced the classic false
  white-on-white (a cursor exiting the frame).
- Contrast failures follow the same persistence rule as layout findings:
  observed at a single sample of a multi-sample sweep demotes to
  warning; held failures gate.

Example fixes the sweep exposed: motion-blur's 21 deliberately-dim rail
labels are marked decorative (its vivid labels pass on their own);
nyt-graph's subtitle and source-note adopt the gate's suggested
compliant gray; decision-tree's declared duration drops 15s to 10s —
its content ends at ~9.5s and every render shipped a blank white tail.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-10 14:08:31 -04:00
parent cea3458016
commit 4b03866c02
10 changed files with 224 additions and 62 deletions
@@ -158,6 +158,22 @@ window.__contrastAuditPrepare = function () {
}
if (!hasText) continue;
// Same decorative opt-out the layout audit honors: text marked (or inside)
// data-layout-ignore is set dressing, not copy a viewer must read —
// deliberately dim rail labels, ghost typography, texture text.
if (el.closest && el.closest("[data-layout-ignore]")) continue;
// Text that has (nearly) left the canvas — a cursor exiting the frame, an
// element parked off-screen — is not readable content, and sampling its
// clamped edge reads whatever pixels happen to sit at the border (the
// classic false "white-on-white"). Require a minimally-visible on-canvas
// intersection before judging contrast; the layout audit separately owns
// off-canvas detection as its own finding class.
var vis = el.getBoundingClientRect();
var onX = Math.min(vis.right, window.innerWidth) - Math.max(vis.left, 0);
var onY = Math.min(vis.bottom, window.innerHeight) - Math.max(vis.top, 0);
if (onX < 8 || onY < 8) continue;
var cs = getComputedStyle(el);
if (cs.visibility === "hidden" || cs.display === "none") continue;
if (parseFloat(cs.opacity) <= 0.01) continue;