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
+40
View File
@@ -25,6 +25,7 @@ has actually finished arriving — never the click that started it.
from __future__ import annotations
import base64
import json
import os
import shutil
@@ -226,6 +227,45 @@ class Browser:
def source(self) -> str:
return self._call("GET", self._s("/source"))["value"]
def screenshot(self, path) -> Path:
"""The viewport as a PNG, written where you ask (v1.1 WP-E).
Evidence for a change a reader judges by looking at it: a contrast ratio
says a boundary is measurable, and a picture says what it looks like.
"""
encoded = self._call("GET", self._s("/screenshot"))["value"]
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(base64.b64decode(encoded))
return target
def hover(self, element: str) -> None:
"""A real pointer over an element, so `:hover` actually applies.
Dispatching a mouseover event from JavaScript does not do this: CSS
`:hover` follows the browser's own pointer state, not a synthetic event,
so a measurement taken after `dispatchEvent` reads the resting style and
reports it as the hover style. This moves the pointer (v1.1 WP-E).
"""
self._call("POST", self._s("/execute/sync"), {
"script": "arguments[0].scrollIntoView({block: 'center', inline: 'nearest'})",
"args": [{ELEMENT_KEY: element}]})
self._call("POST", self._s("/actions"), {"actions": [{
"type": "pointer", "id": "mouse", "parameters": {"pointerType": "mouse"},
"actions": [{"type": "pointerMove", "duration": 60,
"origin": {ELEMENT_KEY: element}, "x": 0, "y": 0}]}]})
def unhover(self) -> None:
"""Move the pointer off whatever it was over, and forget the input state."""
self._call("POST", self._s("/actions"), {"actions": [{
"type": "pointer", "id": "mouse", "parameters": {"pointerType": "mouse"},
"actions": [{"type": "pointerMove", "duration": 30,
"origin": "viewport", "x": 0, "y": 0}]}]})
try:
self._call("DELETE", self._s("/actions"))
except WebDriverError:
pass
def js(self, script: str, *args):
return self._call("POST", self._s("/execute/sync"),
{"script": script, "args": list(args)})["value"]