v1.1: harden recovery and control boundaries

WP-D and WP-E complete the planned v1.1 implementation packages.

WP-D — recovery honesty:
- backups verify the completed copy with PRAGMA integrity_check
- corruption missed by quick_check is detected by the full check
- existing good backups remain protected
- oversized exports are still delivered but declare whether this version can
  import them, while the 20 MB import limit remains unchanged
- backup was exercised through the real browser UI on both the normal campaign
  database and a campaign-shaped database over 100 MB

WP-E — control-boundary contrast:
- interactive control boundaries meet the WCAG 1.4.11 3:1 target
- the contrast audit is now a failing gate rather than an advisory
- rendered browser measurements pass for the composer, controls, tabs and nav
- text contrast and focus visibility remain intact
- owner reviewed and approved the before/after screenshots

Reports:
- planning/reports/v1.1/V1.1-WP-D-REPORT.md
- planning/reports/v1.1/V1.1-WP-E-REPORT.md

All planned v1.1 work packages A-E are now complete. Release validation has not
yet begun.
This commit is contained in:
JesseMarkowitz
2026-09-16 05:37:13 -04:00
parent 59b5ebc2d8
commit 87a40326a2
21 changed files with 2401 additions and 60 deletions
+52 -31
View File
@@ -26,16 +26,24 @@ TOKENS = Path(__file__).resolve().parent.parent.parent / "frontend/src/styles/to
#: rather than combinatorial, because "every colour against every other" reports
#: pairs that never meet on screen.
#:
#: The `kind` matters and is not a way of grading on a curve. **text** pairs are
#: WCAG 1.4.3 Contrast (Minimum) and are what §21 of the M11 brief asks about;
#: they are pass/fail. **boundary** pairs are WCAG 1.4.11 Non-text Contrast,
#: which applies to "visual information required to identify user interface
#: components" — and in this design a control is identified by its *label*,
#: which is measured above and passes, not by its edge. So a boundary below 3:1
#: is reported with its number and does not fail the run; what it would take to
#: turn it into a real failure is a control with no visible label, and there is
#: no such control (`tools/m11_browser.py` asserts every visible control has an
#: accessible name, and the story controls are text buttons).
#: The `kind` records which success criterion a pair is measured against —
#: **text** is WCAG 1.4.3 Contrast (Minimum), **boundary** is WCAG 1.4.11
#: Non-text Contrast — and **both are pass/fail**.
#:
#: v1.1 WP-E overturned the earlier position here, which was that a boundary
#: below 3:1 could be recorded rather than failed because "a control is
#: identified by its label, not by its edge". That argument understates what
#: 1.4.11 asks: the criterion covers the visual information needed to identify
#: a component *and its boundary*, and a reader who cannot see where a text box
#: ends cannot see that there is a text box to type into, label or no label.
#: The edges were at 1.33:1 and 1.75:1 — the tokens were raised instead.
#:
#: Borders are measured against **every background they are drawn on**, and the
#: floor is the worst of them. Inputs and buttons sit on --bg-input, which is
#: lighter than --bg-panel and so the harder case; checking only --bg-panel
#: would have let a token pass the audit while the real control failed.
#: `--bg-panel-glass` is translucent and cannot be resolved from tokens alone;
#: that edge is measured on the rendered page by `tools/m11_browser.py`.
PAIRS = [
("text", "--text", "--bg", 4.5, "body text on the page"),
("text", "--text", "--bg-panel", 4.5, "body text in a panel"),
@@ -48,8 +56,14 @@ PAIRS = [
("text", "--danger", "--bg-panel", 4.5, "an error message"),
("text", "--warning", "--bg-panel", 4.5, "a caution message"),
("text", "--player", "--bg", 4.5, "the player's own words"),
("boundary", "--border", "--bg-panel", 3.0, "a control's resting edge"),
("boundary", "--border-bright", "--bg-panel", 3.0, "a control's hover edge"),
("boundary", "--border", "--bg-input", 3.0, "a field or button's resting edge"),
("boundary", "--border-bright", "--bg-input", 3.0, "a field or button's hover edge"),
("boundary", "--border", "--bg-panel", 3.0, "a control's resting edge in a panel"),
("boundary", "--border-bright", "--bg-panel", 3.0, "a control's hover edge in a panel"),
("boundary", "--border", "--bg", 3.0, "a divider on the page"),
("boundary", "--border-bright", "--bg", 3.0, "the scrollbar thumb"),
("boundary", "--accent-dim", "--bg-input", 3.0, "a focused field's edge"),
("boundary", "--accent-dim", "--bg-panel", 3.0, "a focused control's edge in a panel"),
("boundary", "--chart-1", "--bg-panel", 3.0, "a chart bar"),
("boundary", "--chart-2", "--bg-panel", 3.0, "a chart bar"),
("boundary", "--chart-3", "--bg-panel", 3.0, "a chart bar"),
@@ -81,37 +95,44 @@ def ratio(a: str, b: str) -> float:
def main() -> int:
tokens = read_tokens(TOKENS)
print(f"{TOKENS.relative_to(TOKENS.parents[3])}: {len(tokens)} colour tokens\n")
# Named defensively: the tests run this against a temporary tokens file,
# which need not sit four directories deep the way the real one does.
label = TOKENS.name
if len(TOKENS.parents) > 3:
label = TOKENS.relative_to(TOKENS.parents[3])
print(f"{label}: {len(tokens)} colour tokens\n")
print(f"{'pair':44} {'kind':9} {'ratio':>7} {'floor':>6} verdict")
print("-" * 82)
failures, advisories = 0, 0
text_failures, boundary_failures = 0, 0
for kind, foreground, background, floor, description in PAIRS:
if foreground not in tokens or background not in tokens:
print(f"{description:44} {kind:9} {'—':>7} {floor:>6.1f} MISSING TOKEN")
failures += 1
text_failures += 1
continue
measured = ratio(tokens[foreground], tokens[background])
ok = measured >= floor
if not ok:
if kind == "text":
failures += 1
verdict = "FAIL"
else:
advisories += 1
verdict = "below 1.4.11 (label carries it)"
else:
# Rounded to the two decimals printed, so the verdict matches what the
# reader is shown: a pair reported as 3.00:1 is not failed for arithmetic
# the output does not display.
if round(measured, 2) >= floor:
verdict = "pass"
else:
verdict = "FAIL"
if kind == "text":
text_failures += 1
else:
boundary_failures += 1
print(f"{description:44} {kind:9} {measured:>6.2f}:1 {floor:>6.1f} {verdict}")
print()
if failures:
print(f"{failures} text pair(s) below WCAG AA — this is a defect")
if text_failures:
print(f"{text_failures} text pair(s) below WCAG AA (1.4.3) — this is a defect")
else:
print("every text pair clears WCAG AA (1.4.3)")
if advisories:
print(f"{advisories} boundary pair(s) below 3:1 (1.4.11). Recorded rather "
"than failed: every control in this design carries a visible text "
"label, which is measured above and passes.")
return 1 if failures else 0
if boundary_failures:
print(f"{boundary_failures} boundary pair(s) below 3:1 (WCAG 1.4.11) — "
"this is a defect")
else:
print("every control boundary clears 3:1 (1.4.11)")
return 1 if (text_failures or boundary_failures) else 0
if __name__ == "__main__":