Skip to content

fix(#715): the cod-skeleton HUD failed AAA at 4.51:1 — measured, then fixed - #722

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/715-cod-hud-contrast
Aug 14, 2026
Merged

fix(#715): the cod-skeleton HUD failed AAA at 4.51:1 — measured, then fixed#722
TortoiseWolfe merged 1 commit into
mainfrom
fix/715-cod-hud-contrast

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Pays the coverage debt #721 took on, and the debt was real: the HUD was failing AAA.

What it found

/game/cod-skeleton was excluded from the route-sweeping contrast spec because the sweep cannot settle on a GPU-less runner (#719), and that exclusion said in its own body that the page's contrast was now unmeasured. Measured now — the three HUD chips (stance badge, quality select, controls hint) are all text-base-content on bg-base-300/70 floating over an unconstrained WebGL scene. Bracketing that scene between black and white, worst endpoint per theme:

alpha light dark
/70 5.00:1 4.51:1 ships today — both fail
/85 7.24:1 7.43:1 passes by 1.03×
/90 8.14:1 8.78:1 shipped here

/85 was rejected for the reason globals.css:1716 already rejected it for the twin nav — a 1.03× margin is erased by one theme tweak. Each theme fails at the endpoint where the scene works against the text, and the light case is not even adversarial: ProceduralSky hour={16.5} puts a bright sky directly under the top-left and top-right chips.

How it measures, and why not the cheap ways

jsdom + axe would be vacuous — DaisyUI's stylesheet never applies there, the node lands in incomplete, and toHaveNoViolations() ignores it. Three tests in this repo named for colour contrast already ship green having never measured a ratio. Loading the real route reintroduces #719. Hand-computing the blend measures a colour the browser never paints, since Tailwind emits color-mix(in oklab, …) — that trap once read a real 4.13:1 as 1.03:1.

So the spec loads the route for its stylesheet with WebGL denied via addInitScript, so isWebGLAvailable() returns false, FallbackPanel renders, and no WebGLRenderer is ever constructed. Then it injects the real utility classes, lets the browser resolve them, and reads back through a 1×1 canvas — the only place OKLCH and color-mix become sRGB correctly. Plus a sign-consistency check, so the interior of the bracket cannot dip between two green endpoints.

Three honesty guards, each proven to fire

Every one was mutated on purpose, because a guard never seen to fail is decoration:

  • alpha guard — fired on its own, unprompted, on the first draft: the probe was on /themes/ and read rgba(0, 0, 0, 0), because Next.js splits CSS per route and bg-base-300/70 ships only in the chunk for the route that uses it. It caught me measuring a rule the page never loaded. Re-verified against a nonexistent utility → 3 failed.
  • token guardbase-300 must resolve and differ from base-content.
  • source guard — the classes measured must be the classes shipped. First version was too weak: restyling one of three chips left it green because the other two still matched. Now asserts the count and rejects any chip drifting to another alpha. Re-mutated both ways: restyle one chip → 1 failed; revert one chip to /70 → 1 failed.

Verification

  • 4 passed locally against a real root build, both themes, both endpoints
  • type-check, lint, full unit suite (437 files / 4611 tests), production build, chunk-parse and first-load budget all green via the pre-push gate
  • Needs no GPU by construction — unlike the twin specs it runs identically in the container and in CI

Harness note for the next person, added to the spec header: this needs a real root build (NEXT_PUBLIC_BASE_PATH= DISABLE_BASE_PATH=true pnpm build). DISABLE_BASE_PATH alone is not enough — NEXT_PUBLIC_BASE_PATH from .env wins, absolute gotos then hit the 404 page, and every measurement reads transparent.

Closes #715

🤖 Generated with Claude Code

… fixed

#721 excluded /game/cod-skeleton from the route-sweeping contrast spec because the
sweep cannot settle on a GPU-less runner (#719), and wrote the cost into the
exclusion itself: that page's contrast became unmeasured. This pays that debt, and
the debt turned out to be real.

MEASURED. The HUD is three chips — stance badge, quality select, controls hint — all
`text-base-content` on `bg-base-300/70`, floating over an UNCONSTRAINED WebGL scene.
Bracketing that scene between black and white, worst endpoint per theme:

  /70   light 5.00:1   dark 4.51:1    <- both FAIL the 7:1 gate
  /85   light 7.24:1   dark 7.43:1    <- passes by 1.03x
  /90   light 8.14:1   dark 8.78:1    <- shipped

/85 was rejected for the reason globals.css:1716 already rejected it for the twin nav:
a margin that thin is erased by one theme tweak. Each theme failed at the endpoint
where the scene works against the text — dark over white, light over black — and the
light case is not even adversarial, since ProceduralSky hour=16.5 puts a bright sky
directly under the top-left and top-right chips.

HOW IT IS MEASURED, and why not the obvious ways. jsdom + axe would be vacuous: this
repo already ships three tests named for colour contrast that have never measured a
ratio, because DaisyUI's stylesheet never applies there and the node lands in
`incomplete`, which toHaveNoViolations() ignores. Loading the real route reintroduces
#719. Hand-computing the blend measures a colour the browser never paints — Tailwind
emits color-mix(in oklab, ...), and that trap once read a real 4.13:1 as 1.03:1.

So: load the route for its STYLESHEET with WebGL denied, so isWebGLAvailable() returns
false, FallbackPanel renders, and no WebGLRenderer is ever constructed. Inject the real
utility classes, let the browser resolve them, and read back through a 1x1 canvas —
the only place OKLCH and color-mix become sRGB correctly.

THREE HONESTY GUARDS, EACH PROVEN TO FIRE. This repo keeps getting bitten by tests
that cannot fail, so each was mutated on purpose:

  - alpha guard  — surface must be translucent. Fired immediately and correctly on the
    first draft, which probed on /themes/ and got rgba(0,0,0,0); it caught me measuring
    a rule that page never loaded. Re-verified by pointing it at a nonexistent utility:
    3 failed.
  - token guard  — base-300 must resolve and differ from base-content.
  - source guard — the classes measured must be the classes shipped. FIRST VERSION WAS
    TOO WEAK: restyling one of three chips left it green, because the other two still
    matched. Now asserts the COUNT and rejects any chip drifting to another alpha.
    Re-mutated both ways: restyle one chip -> 1 failed; revert one chip to /70 ->
    1 failed.

The exclusion in color-contrast.spec.ts now names this spec, the way /chatt names
twin-glass-contrast.spec.ts, so the coverage handoff is visible from both ends.

Local harness note for the next person: measuring this needs a REAL root build
(`NEXT_PUBLIC_BASE_PATH= DISABLE_BASE_PATH=true pnpm build`, then `serve out`).
DISABLE_BASE_PATH alone is not enough — NEXT_PUBLIC_BASE_PATH from .env wins, absolute
gotos then hit the 404 page, and every measurement reads transparent.

437 files / 4611 tests, type-check and lint clean.

Closes #715

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cod-skeleton HUD failed AAA at 4.51:1 — the contrast coverage the #721 exclusion owed

2 participants