Codex stop-hook review caught: the detectTextBgContrast ancestor walk
treated any wrapper with a solid `fill` entry as the bg color, even
when the fill was effectively invisible. The classic miss case:
page { fill: cream }
└─ wrapper { fill: [{ type: 'solid', color: '#FFFFFF', opacity: 0 }] }
└─ text { fill: cream }
Without the guard, the detector picked the wrapper's white fill as bg
and reported a healthy contrast ratio against the cream text — masking
the real cream-on-cream failure that lives one level up.
firstSolidColor() now skips fills with `opacity === 0` and 8-hex colors
whose alpha byte is `00` (e.g. `#FFFFFF00`). Both produce no visible
color, so the ancestor walk continues past them to the real bg.
Semi-transparent fills (opacity 0.5, 8-hex alpha 80, etc.) are out of
scope — the detector still treats them as opaque rather than trying to
math the layered composite. Tests pin both: opacity=0.5 + alpha=80
stay treated as bg.
4 new test cases cover the fix plus the boundary (opacity=0.5, alpha=80
should NOT be skipped). Full corpus replay shows 14 hits unchanged on
the 470-row corpus — no false-positive regression introduced.
Two complement scripts that ride alongside replay-detectors.ts:
- inspect-shape.ts: bucket every applied row's root by (width-bucket /
height-bucket / aspect-ratio / child-count). Used 2026-05-10 to
diagnose why detectEdgeSectionPadding scored 0 hits on a 220-row
mobile subset — turned out 49% of mobile rows produce roots with a
string-typed width ("fill_container" / "fit_content") because the
element-tools path emits component fragments, not pages. The
detector predicate `typeof width === 'number'` correctly skips them
→ 0 hits is the EVAL HARNESS coverage gap, not a detector bug.
- inspect-issue-category.ts: print every issue in a chosen category
with row id + node id + reason. Generic version of inspect-contrast-
hits.ts. 2026-05-10 used it to validate
excessive-frame-effects (4/4 TP — blur 48 cards + OTP slot spread)
and the two borderline mixed-sibling hits (header padding outlier,
spacer cornerRadius outlier — known role-aware limitation, 0.2%
noise rate, accepted).
Together with replay-detectors.ts these three give a fast empirical
loop for tuning a detector against real corpus output without burning
fresh API tokens.