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.
375 lines
19 KiB
Markdown
375 lines
19 KiB
Markdown
# v1.1 WP-E — Control-Boundary Contrast
|
||
|
||
**Status:** COMPLETE — **PASS**, with owner approval of the screenshots
|
||
outstanding. The decision, and what was deliberately not claimed, is in §O.
|
||
|
||
---
|
||
|
||
## A. Repository baseline
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| Branch | `v1.1-development` |
|
||
| HEAD | `59b5ebc` — *v1.1 WP-C: browser release coverage*, signed by the owner |
|
||
| Working tree at start | WP-D staged (10 files), nothing committed |
|
||
| Criterion | WCAG 2.1 **1.4.11 Non-text Contrast**, 3:1, for control boundaries; **1.4.3** 4.5:1 for body text, unchanged |
|
||
|
||
---
|
||
|
||
## B. What v1.0.0 actually did
|
||
|
||
`tools/contrast_audit.py` measured control boundaries, printed that two of them
|
||
were below 3:1, and **exited 0**. Its own comment argued the position:
|
||
|
||
> 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.
|
||
|
||
So the audit was a report, not a gate: no palette change could ever fail it on a
|
||
boundary. The two numbers it printed were **1.33:1** (`--border` on
|
||
`--bg-panel`) and **1.75:1** (`--border-bright`), against a floor of 3.0.
|
||
|
||
**WP-E overturns that argument.** 1.4.11 covers the visual information needed to
|
||
identify a component *and its boundary*; 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
|
||
tokens were raised rather than the criterion re-argued.
|
||
|
||
---
|
||
|
||
## C. Token inventory
|
||
|
||
| Token | v1.0.0 | v1.1 | Why |
|
||
| --- | --- | --- | --- |
|
||
| `--border` | `#2b2b3d` | **`#676792`** | every control's resting edge |
|
||
| `--border-bright` | `#3d3d55` | **`#7a7aaa`** | hover edges, the composer's resting edge, the open panel tab |
|
||
| `--bg-panel`, `--bg-input`, `--bg`, `--text`, `--text-dim`, `--accent*`, `--danger`, `--warning`, `--player`, `--chart-*` | — | **unchanged** | WP-E is a boundary package; no text or accent colour moved |
|
||
|
||
**The floor is taken against `--bg-input`, not `--bg-panel`.** Inputs and buttons
|
||
are drawn on `--bg-input` (`styles/forms.css`), which is lighter than
|
||
`--bg-panel` and therefore the harder case. The audit had been checking only
|
||
`--bg-panel`, so a token could have passed the audit while the real control
|
||
failed. Measured on the new values:
|
||
|
||
| | vs `--bg-input` | vs `--bg-panel` | vs `--bg` |
|
||
| --- | --- | --- | --- |
|
||
| `--border` | **3.21:1** | 3.44:1 | 3.70:1 |
|
||
| `--border-bright` | **4.24:1** | 4.55:1 | 4.88:1 |
|
||
|
||
Two properties were preserved deliberately: the rest→hover step is the same size
|
||
as before (1.318 → 1.320), so hover still reads as a change rather than a jump;
|
||
and `--border-bright` stays *below* body text against the same panel (2.98:1
|
||
between them), so no edge outshines the words inside it.
|
||
|
||
---
|
||
|
||
## D. Component inventory
|
||
|
||
Where these tokens are actually drawn, from the stylesheets:
|
||
|
||
| Control | Rule | Rest | Hover / active | Focus |
|
||
| --- | --- | --- | --- | --- |
|
||
| Story composer | `.input-bar` (story.css) | `--border-bright` on `--bg-panel` | — | `--accent-dim` + `--accent-glow` ring |
|
||
| Story controls | `.story-controls button` | `--border` on `--bg-panel` | `--border-bright` | (M11 focus check) |
|
||
| Fields and buttons | `forms.css` | `--border` on `--bg-input` | `--accent-dim` | `--accent-dim` + ring |
|
||
| Panel tabs | `.panel-tabs button` | **`transparent`** | `--border` on hover, `--border-bright` when active | — |
|
||
| Top navigation | `.topnav` | `--border` bottom edge on **`--bg-panel-glass`** | — | — |
|
||
| Scrollbar thumb | `base.css` | `--border-bright` as a *fill* on `--bg` | `--accent-dim` | — |
|
||
|
||
Two of these cannot be answered by token arithmetic at all, and both are
|
||
measured in the browser instead (§G): the nav sits on a translucent panel, and
|
||
the panel tab's edge is `transparent` until the panel is open.
|
||
|
||
---
|
||
|
||
## E. The audit is now a gate
|
||
|
||
`tools/contrast_audit.py`:
|
||
|
||
1. **Boundary pairs fail.** `text` and `boundary` rows are both pass/fail; the
|
||
advisory branch is gone. The run returns 1 if either kind falls short.
|
||
2. **Eight boundary pairs replace two.** Each border is checked against every
|
||
background it is drawn on — `--bg-input`, `--bg-panel` and `--bg` — plus the
|
||
focused edge (`--accent-dim`) on both panel and field backgrounds.
|
||
3. **The verdict is taken on the number that is printed** (rounded to two
|
||
decimals), so a pair shown as `3.00:1` is not failed for arithmetic the
|
||
reader cannot see.
|
||
4. The comment block that argued the old position is replaced by one recording
|
||
what changed and why, including why `--bg-panel-glass` is not in the list.
|
||
|
||
## F. Gate tests
|
||
|
||
`backend/tests/test_v11_e_contrast.py` — **11 passed**. The threshold is
|
||
exercised from both sides, on real token files:
|
||
|
||
| Test | Result |
|
||
| --- | --- |
|
||
| A boundary at **2.99:1** against `--bg-input` fails the run (exit 1) | PASS |
|
||
| A boundary at **3.00:1** passes (exit 0) | PASS |
|
||
| The **v1.0.0 value** `#2b2b3d` fails, at 1.24:1 against `--bg-input` | PASS |
|
||
| A dimmed `--text-dim` still fails as a *text* pair | PASS |
|
||
| A renamed token is a failure, not a silent skip | PASS |
|
||
| The shipped palette passes both criteria | PASS |
|
||
| Every boundary pair is measured against the background it is drawn on | PASS |
|
||
| The M11 text baselines are unchanged: 14.57 / 13.57 / 5.48 / 5.88 | PASS |
|
||
| The hover edge stays brighter than the resting edge | PASS |
|
||
| No boundary becomes as loud as body text | PASS |
|
||
| The WCAG ratio formula is anchored on known values (21:1, 1:1, symmetry) | PASS |
|
||
|
||
The 2.99 and 3.00 values are worth noting: **both clear 3:1 against
|
||
`--bg-panel`** (3.21 and 3.22). They decide the gate only because the floor is
|
||
now taken against the background the control is really on — so these two tests
|
||
also prove §C's change is doing work.
|
||
|
||
---
|
||
|
||
## G. Browser measurement
|
||
|
||
`tools/m11_browser.py` gains a third suite, **WP-E**, counted separately from
|
||
M11's 38 and WP-C's 53. It measures the *rendered* edge — `borderColor` from
|
||
`getComputedStyle` — against what is actually behind it, with every translucent
|
||
layer composited bottom-up.
|
||
|
||
A boundary is measured against **both** adjacent colours (the control's own fill
|
||
inside it, the background outside it) and passes on the better of the two: an
|
||
edge that matches its fill but contrasts with the page is still a visible
|
||
outline. What 1.4.11 asks is that the component's extent be perceivable.
|
||
|
||
Two harness capabilities were added for this (`tools/m11_webdriver.py`):
|
||
|
||
- **`hover()`** moves a real pointer through the WebDriver Actions API.
|
||
Dispatching a `mouseover` event from JavaScript does *not* trigger CSS
|
||
`:hover`, so a synthetic event would have re-measured the resting edge and
|
||
reported it as the hover edge.
|
||
- **`screenshot()`** writes the viewport as a PNG, for the before/after evidence.
|
||
|
||
**A defect this found in my own first measurement.** The first run reported the
|
||
hover edge as `rgb(114, 114, 160)` and the focused edge as `rgb(144, 120, 81)` —
|
||
neither of which is any token. Both controls carry `transition: border-color
|
||
0.15s`, so the measurement was taken mid-animation, on a colour no state
|
||
actually has. `_settled()` now polls until the computed edge colour is the same
|
||
on two consecutive reads before measuring (polled, not slept, per this harness's
|
||
own rule). After the fix the same edges read exactly `rgb(122, 122, 170)`
|
||
(`--border-bright`) and `rgb(150, 119, 58)` (`--accent-dim`).
|
||
|
||
---
|
||
|
||
## H. Before and after, measured in the browser
|
||
|
||
Both passes were taken the same way — `--only boundaries --no-narrator`, the
|
||
production build — with only `tokens.css` differing. Evidence under
|
||
`$HOME/v11-evidence/wp-e/before/` and `.../after/`.
|
||
|
||
| Control (state) | Before | After | Floor |
|
||
| --- | --- | --- | --- |
|
||
| Story composer — resting edge | **1.88:1** FAIL | **4.88:1** pass | 3.0 |
|
||
| Story control — resting edge | **1.43:1** FAIL | **3.70:1** pass | 3.0 |
|
||
| Open panel tab — resting edge | **1.75:1** FAIL | **4.55:1** pass | 3.0 |
|
||
| Story control — hover edge | **1.88:1** FAIL | **4.88:1** pass | 3.0 |
|
||
| Top navigation — translucent edge | **1.43:1** FAIL | **3.70:1** pass | 3.0 |
|
||
| Story composer — focused edge | 4.70:1 pass | 4.70:1 pass | 3.0 |
|
||
|
||
**Suite result: before 5 passed / 5 failed; after 10 passed / 0 failed / 0
|
||
skipped.**
|
||
|
||
Two things this table says that a summary would blur:
|
||
|
||
- **Focus was never the defect.** The focused edge (`--accent-dim`) already
|
||
cleared 3:1 in v1.0.0 at 4.70:1, and WP-E did not change it. What failed was
|
||
rest and hover — the states a reader spends all their time in.
|
||
- **The translucent edge is real evidence.** The nav's background composited to
|
||
`rgb(17, 17, 29)` — `--bg-panel-glass` (rgba 19,19,32 @ 0.82) over
|
||
`rgb(10, 10, 15)` — not a fallback. That is the case token arithmetic cannot
|
||
reach, and it moved from 1.43:1 to 3.70:1.
|
||
|
||
## I. Screenshots
|
||
|
||
| File | |
|
||
| --- | --- |
|
||
| `before/control-boundaries.png` | 131,175 bytes, 1366×682 |
|
||
| `before/control-boundaries-nav.png` | 63,376 bytes, 1366×682 |
|
||
| `after/control-boundaries.png` | 132,430 bytes, 1366×682 |
|
||
| `after/control-boundaries-nav.png` | 63,541 bytes, 1366×682 |
|
||
|
||
All four are PNG, 1366×682, taken on the production build through the same
|
||
harness path, differing only in `tokens.css`. The play-page pair shows the
|
||
composer, the story controls and the open panel tab; the nav pair shows the
|
||
translucent top edge on the library route.
|
||
|
||
## J. A finding this package created and fixed
|
||
|
||
Raising `--border-bright` broke something that had nothing to do with control
|
||
boundaries. `.slice-7` in the context inspector's token breakdown was painted
|
||
with `var(--border-bright)`, so it followed the token to `#7a7aaa` — an OKLab ΔE
|
||
of **0.035** from `.slice-6` (`#7c86b8`), making two neighbouring chart slices
|
||
effectively the same colour. The other slices sit **0.100–0.119** from their
|
||
nearest neighbour.
|
||
|
||
`.slice-7` is now pinned to `#3d3d55`, the literal value it already rendered, so
|
||
its appearance is unchanged from v1.0.0 and its separation (ΔE **0.251**) is the
|
||
widest in the set. A chart fill and a control edge have different jobs and should
|
||
not share a token.
|
||
|
||
Reassigning it to a fresh hue was considered and rejected on evidence: inside the
|
||
palette's own chroma (0.045–0.120) and lightness (0.586–0.804) bands, the only
|
||
hues clearing the set's 0.100 separation floor are pinks near 14°, which is
|
||
`--danger`'s territory. Painting an ordinary prompt section in the colour this
|
||
application reserves for failure would trade an accessibility fix for a semantic
|
||
lie.
|
||
|
||
*(A first attempt, `#5d7f9e`, was rejected by the same measurement at ΔE 0.061 —
|
||
below every real slice. It is recorded here because it was written into the file
|
||
before it was measured.)*
|
||
|
||
## K. Text contrast regression
|
||
|
||
Unchanged, and asserted so in §F: **14.57:1** body text on the page, **13.57:1**
|
||
in a panel, **5.48:1** secondary text in a panel, **5.88:1** on the page. Every
|
||
text pair still clears 1.4.3, and no text token was touched.
|
||
|
||
## L. Browser release regression
|
||
|
||
The full harness, all three suites, against a real narrator on the production
|
||
build. Evidence: `$HOME/v11-evidence/wp-e/release/browser-report.json`.
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| Kind | **`release regression`** — not `partial`, not `development (only …)` |
|
||
| Narrator | `qwen2.5:3b-instruct`, the reference model, over plain HTTP on the LAN GPU host |
|
||
| Browser | Firefox 155.0.1, geckodriver 0.37.1 |
|
||
| Served | FastAPI on loopback, the **built** SPA (`dist` 2026-09-15T21:08:52) |
|
||
| Duration | 71 s |
|
||
| Turns played | **8**, of which `turns_not_clean` **0** and `protocol_shapes_in_narration` **0** |
|
||
|
||
**Counted per suite, as the brief requires:**
|
||
|
||
| Suite | Passed | Failed | Skipped |
|
||
| --- | --- | --- | --- |
|
||
| **M11** (the v1 release regression) | **38** | **0** | **0** |
|
||
| **WP-C** (browser release coverage) | **53** | **0** | **0** |
|
||
| **WP-E** (control boundaries) | **10** | **0** | **0** |
|
||
| **Total** | **101** | **0** | **0** |
|
||
|
||
M11's 38 and WP-C's 53 are unchanged in count and in name: WP-E added a suite
|
||
beside them rather than altering either. The `kind` field is quoted above
|
||
because a fast run invites the question — 71 s for 101 checks including 8
|
||
narrated turns is the GPU host being quick with a 3B model, and the 8 recorded
|
||
turns with no unclean accounting are what rule out narration having been
|
||
skipped.
|
||
|
||
The WP-E rows in this run are the same ten as the standalone capture in §H,
|
||
re-measured with a narrator present and a full story on the page.
|
||
|
||
## M. Full regression
|
||
|
||
| Suite | Result |
|
||
| --- | --- |
|
||
| **Full backend suite** (`pytest -q`, no `AIDND_TEST_*` set) | **1,723 passed, 17 skipped, 0 failed, 0 xfailed** (1,054.7 s) |
|
||
| **Frontend suite** (`npm test`) | **175 passed**, 15 files, 0 failed |
|
||
| **Lint** (`npm run lint`, oxlint) | **exit 0**, 0 errors, 15 warnings |
|
||
| **Production build** (`npm run build`) | succeeded |
|
||
| **Browser harness** (M11 + WP-C + WP-E) | **101 passed, 0 failed, 0 skipped** (§L) |
|
||
| **Contrast audit** (`python tools/contrast_audit.py`) | **exit 0** — every text pair and every boundary pair passes |
|
||
|
||
**The backend count reconciles exactly.** WP-D's tree was 1,712; WP-E adds the
|
||
11 in `test_v11_e_contrast.py`. 1,712 + 11 = **1,723**. The 17 skips are the same
|
||
environment-gated real-model tests recorded since B.1 — WP-E used no model and
|
||
added no skip.
|
||
|
||
**The plan's regression requirements for WP-E** were the frontend suite and
|
||
lint, and the harness's accessibility checks. All three pass: 175 and exit 0
|
||
above, and M11's `A11y` rows — accessible names, visible keyboard focus, no
|
||
positive tabindex, nothing revealed only on hover, and the four rendered text
|
||
contrasts — are inside the 38/38 in §L.
|
||
|
||
**Lint detail.** The 15 warnings are the same pre-existing
|
||
`only-export-components` and unused-import kind recorded at WP-C and WP-D, and
|
||
**none is in a file WP-E changed**. `tokens.css` and `context.css` are not
|
||
flagged.
|
||
|
||
## N. Residual risks
|
||
|
||
1. **The screenshots are unapproved.** The measurements say every boundary now
|
||
clears 3:1; whether the result *looks* right in this design is a judgment
|
||
the numbers cannot make. Recorded as PENDING in §O, not assumed.
|
||
2. **Five controls are measured in the browser; the rest inherit.** The audit
|
||
checks token pairs, and the harness measures the composer, a story control,
|
||
the open panel tab, the nav edge and the focused composer. Every other
|
||
bordered surface — modals, cards, the knowledge and context panels — draws
|
||
the same two tokens, so it moves with them, but none is individually
|
||
measured. A component that overrides a border with a literal colour would
|
||
not be caught by either check.
|
||
3. **`--bg-panel-glass` is outside the token audit by nature.** It is rgba over
|
||
a gradient, so no token pair can express it; its edge is covered only by the
|
||
browser measurement, which runs in the harness rather than in CI.
|
||
4. **Disabled controls are deliberately not measured.** `.story-controls
|
||
button:disabled` carries `opacity: 0.35`, so a disabled control's rendered
|
||
edge is dimmer than any value here. WCAG 1.4.11 exempts inactive components,
|
||
and the harness selects `:not(:disabled)` on purpose — stated so that the
|
||
exclusion is visible rather than looking like an oversight.
|
||
5. **The chart set was re-checked only where WP-E disturbed it.** `.slice-7`'s
|
||
separation and colour-blind distance were measured against the other seven
|
||
(§J); the set as a whole was not re-audited, which is outside this package.
|
||
6. **`a11y.test.jsx` was not extended**, though the plan listed it as likely
|
||
affected. It asserts structure and names, not colours, and adding colour
|
||
assertions in jsdom would test the stylesheet's text rather than a rendered
|
||
result. Boundary contrast is asserted instead where it can be measured: the
|
||
gate tests (§F) and the browser (§G).
|
||
|
||
**One risk that turned out not to exist.** §G's rule — a boundary passes on the
|
||
better of its two adjacent colours — was written to avoid failing an edge that
|
||
contrasts with the page but matches its own fill. In the event it never did any
|
||
work: every measured boundary clears 3:1 against **both** neighbours (composer
|
||
4.55/4.88, story control 3.44/3.70, panel tab 4.24/4.55, hover 4.55/4.88, focus
|
||
4.38/4.70, nav 3.51/3.70). The stricter reading would have produced the same
|
||
verdict on every row.
|
||
|
||
## O. Final decision
|
||
|
||
**Against the plan's acceptance criteria** (§ WP-E, *Control-boundary contrast*):
|
||
|
||
| # | Criterion | Verdict |
|
||
| --- | --- | --- |
|
||
| 1 | `tools.contrast_audit` exits non-zero on any boundary pair below 3:1, and 0 on the package tree | **PASS** — 2.99:1 exits 1 and 3.00:1 exits 0 (§F); the v1.0.0 value exits 1; the package tree exits 0 |
|
||
| 2 | Every text pair still clears 4.5:1, and the rendered text contrasts do not fall below v1's 14.57 / 5.48 / 13.57 / 5.88 | **PASS** — asserted as exact baselines in §F and re-measured on the rendered page in §L's M11 rows. No text token changed |
|
||
| 3 | The rendered boundary of the story input and of a primary control, at rest and on hover, is at least 3:1; the focus indicator is still visible | **PASS** — composer 4.88:1, story control 3.70:1 at rest and 4.88:1 on hover (§H), and M11's visible-focus check passes in §L |
|
||
| 4 | The owner approves the before-and-after screenshots, and the report records the approval | **PENDING** — not a criterion I can satisfy. The four PNGs are in §I |
|
||
|
||
**Where I exceeded the criterion, said plainly.** The plan asks for 3:1 "against
|
||
their panel" and names two controls. This package measures against
|
||
**`--bg-input`** as well — the lighter background inputs and buttons are really
|
||
drawn on, and the one that decides the gate — and adds the open panel tab, the
|
||
translucent navigation edge and the focused state. The stricter floor is the
|
||
reason the two threshold tests in §F are decided by `--bg-input` rather than
|
||
`--bg-panel`, where both would have passed.
|
||
|
||
**What I got wrong and corrected.** Three of my own claims failed checking and
|
||
were fixed rather than softened: the first hover and focus measurements were
|
||
taken mid-transition and reported colours no state has (§G); two contrast
|
||
figures were written into `context.css` before being measured, and were wrong
|
||
(§J); and my first gate check reported the current tokens as failing because my
|
||
harness crashed on a shallow path, not because of any contrast.
|
||
|
||
**Not done, and not claimed:** owner approval (criterion 4), individual
|
||
measurement of every bordered component (§N.2), and any palette work beyond the
|
||
two boundary tokens and the one chart fill that borrowed from them.
|
||
|
||
```text
|
||
WP-E CONTRAST GATE: PASS
|
||
WP-E BOUNDARY MEASUREMENT: PASS
|
||
|
||
WP-E OVERALL:
|
||
PASS, pending owner approval of the screenshots (criterion 4)
|
||
```
|
||
|
||
All WP-E changes are **staged and uncommitted**. No commit, no push, no tag.
|
||
v1.1 release validation has not begun.
|
||
|
||
```text
|
||
OWNER SCREENSHOT APPROVAL: PENDING
|
||
```
|
||
|
||
The before/after screenshots in §I are the evidence for a change a reader judges
|
||
by looking at it. The measurements say every boundary now clears 3:1; whether
|
||
the result looks right in this design is the owner's call, and it is recorded as
|
||
pending rather than assumed.
|