From 732eca6fc600d0e526428dcb6973dd8fea30ce47 Mon Sep 17 00:00:00 2001 From: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:17:34 -0700 Subject: [PATCH 1/7] test(cli): scope the empty-list assertion to a project the test owns test_list_on_a_project_with_no_matching_items_exits_zero_with_empty_list asserted `items == []` for `--status deferred` against the SESSION-scoped `shared_project_name`, with no filter narrowing it to anything this test controls: it requested `unique_lane` and never used it, because the CLI's `list` has no lane filter to pass it to. Measured on the untouched tree: pytest tests/integration/test_defer_block.py -q -> 1 failed, 21 passed (6 leaked deferred items) alone -> 1 passed Order-dependence, not a regression. Fixed by giving the test its own project via `unique_project_name` (+ its existing teardown), matching what the adapter-level siblings already do via `project_factory` (test_work_list.py::test_list_bounded_on_empty_project..., test_list_via_sql_equivalence.py::test_list_on_empty_project...). The project deliberately holds one OPEN item, so the assertion still proves the *filter* matched nothing rather than that the project is empty. pytest tests/integration/test_defer_block.py -q -> 22 passed pytest tests/cli -q -> 93 passed, 1 pre-existing failure (test_doctor_quick_succeeds_against_the_real_installed_bd) Swept tests/cli and tests/integration for the same shape (exact emptiness/count assertion on the shared project with no lane filter): no other instance. Every other exact-count assertion is either on a per-test project (unique_project_name / project_factory), lane-filtered, membership-based, or an inequality. --- tests/cli/test_cli_surface.py | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/cli/test_cli_surface.py b/tests/cli/test_cli_surface.py index 4e450c1..d0453d8 100644 --- a/tests/cli/test_cli_surface.py +++ b/tests/cli/test_cli_surface.py @@ -304,12 +304,40 @@ def snapshot(): def test_list_on_a_project_with_no_matching_items_exits_zero_with_empty_list( - run_cli, shared_project_name, unique_lane + run_cli, unique_project_name ): - """An unused lane guarantees zero items for THIS status filter, without - needing a brand-new project -- proving the empty case is a normal, - non-error outcome, not a crash.""" - result = run_cli(["list", "--project", shared_project_name, "--status", "deferred", "--json"]) + """A project this test owns outright guarantees zero items for THIS + status filter -- proving the empty case is a normal, non-error outcome, + not a crash. + + Scoped to its own project rather than the session-shared one on + purpose. This is an EXACT emptiness assertion, and + ``shared_project_name`` is session-scoped across ``tests/integration`` + and ``tests/cli`` -- ``tests/integration/test_defer_block.py`` defers + items into it, so this assertion passed when the test ran alone and + failed the moment that module ran first (measured: 6 leaked deferred + items). That is collection order, not a regression. The earlier + version asked for ``unique_lane`` to scope itself and then never used + it, because ``list`` has no lane filter to pass it to -- an owned + project is the only scope this test actually controls. The + adapter-level siblings already do exactly this + (``test_work_list.py::test_list_bounded_on_empty_project_returns_empty_not_an_error``, + ``test_list_via_sql_equivalence.py::test_list_on_empty_project_returns_empty_not_an_error``, + both via ``project_factory``). + + The project is deliberately NOT empty -- it holds one open item -- so + this still proves the *filter* matched nothing, rather than merely that + the project has nothing in it. + """ + project = unique_project_name + created = run_cli(["new", project]) + assert created.returncode == 0, created.stderr + added = run_cli( + ["add", "--project", project, "an open item the deferred filter must not match"] + ) + assert added.returncode == 0, added.stderr + + result = run_cli(["list", "--project", project, "--status", "deferred", "--json"]) assert result.returncode == 0, result.stderr body = json.loads(result.stdout) assert body["items"] == [] From 37ba92f150ebda3385d6a69e5509b70a3da64216 Mon Sep 17 00:00:00 2001 From: amplifier Date: Sat, 5 Sep 2026 15:44:01 -0700 Subject: [PATCH 2/7] highway(operator-surface) rendered floors: Core 7's three failing floors met in the render, OSV1-010 -> CONFORMS Core 7's Tier-B half asked for computed contrast ratios, target bounding boxes and a motion trace at 430/900/1280 in both themes, re-checked against the floors. Three of the four floors failed. All four now pass, measured in chromium 148 over 18 renders and recorded three times with `perception.floors` byte-identical on every asserted field. TEXT (4.5:1): 7 nodes below -> 0. Fixed at the TOKEN in all four declared blocks -- `--ink-quiet` #7c8798 -> #a1a8b5 dark, #596473 -> #4e5764 light; `--brand-cyan-ink` #0b6b80 -> #0a5e71 light. This is the honest limit OSV1-009 recorded: the same tokens cleared 4.5:1 as flat swatch pairs and sat at 3.13-4.35:1 on the real glass-over-glass chip and drawer surfaces. The ink ramp did not collapse to buy it (tertiary reads 7.33:1 / 5.75:1 on the same surface). TARGETS (44px): 26 of 34 controls on L0 -> 0, and 0 of 41 on L1 / 0 of 20 on L2 at every width. The pause control (26x26), nav icon buttons and theme toggle (34x34), window and status tabs (28px tall), activity-feed rows (37px), the L1 filter input (181x19), breadcrumb, wordmark and identity links (11.5-21px) all reach --u. For the link-shaped ones the HIT AREA grew and the type did not; the 62px header keeps its height and `elements_beyond_viewport` stays 0 at 430px. NON-TEXT (3:1): 16 of 79 on L0 -> 0 in both themes (L1 23/73 -> 0, L2 11/33 -> 0). One new token, `--control-edge` (rgba(255,255,255,.40) dark / rgba(11,18,32,.52) light), carries the boundary of an INTERACTIVE control; `--glass-hairline`/`-soft` keep painting the decorative panel edges WCAG 1.4.11 exempts, so a rim did not become a stroke everywhere. Worst case after: 3.35:1 dark, 3.23:1 light. Neutral by construction, no new hue. ONE BLOCKED RESIDUAL, enumerated and machine-checked: L1's status-mix donut BACKING RING (`.donut-track`/`.donut-hatch-gap`, 1.32:1 dark / 1.15:1 light) is exempt via `_probe.NON_TEXT_EXEMPT_CLASSES`. At the nearest passing colour the empty ring separates from a real --blocked arc by 1.60:1 dark / 2.25:1 light -- less than the floor being asked for -- so meeting it would make a five-status donut read as six. The exemption is subtracted by `below_non_text_floor`, returned by `exempt_below_non_text_floor`, counted per render as `non_text_exempt_below_floor`, and pinned twice: by the kit (`test_the_non_text_exemption_stays_narrow`) and by the row's own probe. LEDGER: OSV1-010 VIOLATION -> CONFORMS, `test_row_osv1_010` retargeted to the regression direction over all 18 renders, its four Tier-B xfail(strict) markers deleted, and its mutation replaced by two regressions (a control falling back under 44px; the exemption growing). OSV1-031 re-pinned 4 -> 3 red Core rows. `_mo009a/b` and `_mo011` re-anchored to the new token value and the new recording. OSV1-006's line-keyed exemption register re-anchored +82 lines (same three sites, no growth) -- the one out-of-scope edit, forced by comments added above them. --- ledger/checks/mutation_harness.py | 57 +++++-- ledger/checks/test_operator_rows.py | 146 +++++++++++----- ledger/rows.yaml | 152 ++++++++++++++++- src/amplifier_work_tracker/webtheme.py | 156 ++++++++++++++---- .../operator_surface/browser/LAST_RUN.json | 150 +++++++++-------- .../operator_surface/browser/_probe.py | 62 ++++++- .../operator_surface/browser/test_tier_b.py | 104 +++++++----- 7 files changed, 625 insertions(+), 202 deletions(-) diff --git a/ledger/checks/mutation_harness.py b/ledger/checks/mutation_harness.py index 1f3c33d..d78ae5b 100644 --- a/ledger/checks/mutation_harness.py +++ b/ledger/checks/mutation_harness.py @@ -507,14 +507,14 @@ def _mo009a_the_media_light_block_regresses(w: World) -> None: first would credit a half-reverted colour as conformance -- which is exactly the shape the seed pin recorded (three grounds x TWO blocks = six pairs). """ - w.replace(WEBTHEME, " --ink-quiet:#596473;", " --ink-quiet:#7c8ba0;") + w.replace(WEBTHEME, " --ink-quiet:#4e5764;", " --ink-quiet:#7c8ba0;") def _mo009b_the_attr_light_block_regresses(w: World) -> None: """REGRESSION: the same fallback in the manual-toggle light block.""" w.replace( WEBTHEME, - " --ink-quiet:#596473; /* likewise", + " --ink-quiet:#4e5764; /* likewise", " --ink-quiet:#7c8ba0; /* likewise", ) @@ -678,14 +678,40 @@ def _mo008_the_pause_control_starts_surviving(w: World) -> None: ) -def _mo010_the_target_floor_is_met(w: World) -> None: - """FIXED: every interactive control on L0 reaches 44px.""" +def _mo010_a_control_falls_back_under_the_target_floor(w: World) -> None: + """REGRESSION: the browser measures a control back under 44px on L0. + + RETARGETED 2026-09-05 with the row (work_item_pipeline-96f). This used to + inject the FIX -- `controls_below_44px` 26 -> 0 -- because OSV1-010 was a + pin. The row now reads CONFORMS and asserts the floors are MET, so the + counterfactual that has to go red is the shape it forbids: one control + slipping back under the floor on ONE render of eighteen. The probe sweeps + every render for exactly this reason. + """ w.replace( TIER_B_SUMMARY, '"calm/L0/1280/dark": {\n "client_width": 1280,\n "controls": 34,\n' - ' "controls_below_44px": 26,', - '"calm/L0/1280/dark": {\n "client_width": 1280,\n "controls": 34,\n' ' "controls_below_44px": 0,', + '"calm/L0/1280/dark": {\n "client_width": 1280,\n "controls": 34,\n' + ' "controls_below_44px": 1,', + ) + + +def _mo010_the_donut_exemption_grows(w: World) -> None: + """REGRESSION: the ONE enumerated non-text exemption widens. + + The failure mode an exemption invites: the non-text arm stays green not + because the borders were fixed but because the allowance grew to cover + them. A second exempted element on L1 must go red here, and does. + """ + w.replace( + TIER_B_SUMMARY, + '"calm/L1/1280/dark": {\n "client_width": 1280,\n "controls": 41,\n' + ' "controls_below_44px": 0,\n "elements_beyond_viewport": 0,\n' + ' "non_text_below_floor": 0,\n "non_text_exempt_below_floor": 1,', + '"calm/L1/1280/dark": {\n "client_width": 1280,\n "controls": 41,\n' + ' "controls_below_44px": 0,\n "elements_beyond_viewport": 0,\n' + ' "non_text_below_floor": 0,\n "non_text_exempt_below_floor": 4,', ) @@ -701,13 +727,15 @@ def _mo011_an_animation_runs_under_the_preference(w: World) -> None: w.replace( TIER_B_SUMMARY, '"calm/L0/430/dark": {\n "client_width": 430,\n "controls": 34,\n' - ' "controls_below_44px": 16,\n "elements_beyond_viewport": 0,\n' - ' "non_text_below_floor": 16,\n "non_text_measured": 77,\n' + ' "controls_below_44px": 0,\n "elements_beyond_viewport": 0,\n' + ' "non_text_below_floor": 0,\n "non_text_exempt_below_floor": 0,\n' + ' "non_text_measured": 77,\n' ' "overflow_x_style": "clip",\n' ' "running_animations_under_reduced_motion": 0,', '"calm/L0/430/dark": {\n "client_width": 430,\n "controls": 34,\n' - ' "controls_below_44px": 16,\n "elements_beyond_viewport": 0,\n' - ' "non_text_below_floor": 16,\n "non_text_measured": 77,\n' + ' "controls_below_44px": 0,\n "elements_beyond_viewport": 0,\n' + ' "non_text_below_floor": 0,\n "non_text_exempt_below_floor": 0,\n' + ' "non_text_measured": 77,\n' ' "overflow_x_style": "clip",\n' ' "running_animations_under_reduced_motion": 6,', ) @@ -1109,8 +1137,13 @@ def _mo034_the_changelog_records_a_look(w: World) -> None: ), Mutation( "OSV1-010", - "the browser measures every interactive control on L0 reaching 44px (the fix)", - _mo010_the_target_floor_is_met, + "the browser measures one interactive control back under 44px on a swept render", + _mo010_a_control_falls_back_under_the_target_floor, + ), + Mutation( + "OSV1-010", + "the one enumerated non-text exemption grows to cover more than the donut track", + _mo010_the_donut_exemption_grows, ), Mutation( "OSV1-011", diff --git a/ledger/checks/test_operator_rows.py b/ledger/checks/test_operator_rows.py index 54108d8..55f48e2 100644 --- a/ledger/checks/test_operator_rows.py +++ b/ledger/checks/test_operator_rows.py @@ -537,9 +537,13 @@ def test_row_osv1_005() -> None: "webapp.py:1127", # flex:{n} 1 0 -- state-bar segment ratio "webapp.py:1823", # width:{today_w}px -- throughput bar, today "webapp.py:1826", # width:{prior_w}px -- throughput bar, prior 6d - "webtheme.py:4197", # {style} -- axis ruler numeral offset - "webtheme.py:4216", # left:{_grad_x(f):.1f}px -- graduation tick offset - "webtheme.py:4223", # width:{px}px -- age bar length + # Re-anchored 2026-09-05 (work_item_pipeline-96f, OSV1-010): +82 lines + # each, the token-block comments the Core 7 rendered-floor fix added + # above them. Same three sites, same code, same reason -- a line-keyed + # register moves whenever anything above it does. + "webtheme.py:4279", # {style} -- axis ruler numeral offset + "webtheme.py:4298", # left:{_grad_x(f):.1f}px -- graduation tick offset + "webtheme.py:4305", # width:{px}px -- age bar length "widgets.py:837", # width:{pct}% -- status-mix segment (hatched) "widgets.py:839", # width:{pct}% -- status-mix segment } @@ -770,45 +774,108 @@ def test_row_osv1_009() -> None: def test_row_osv1_010() -> None: - """Core 7 VIOLATION pin (rendered half), RE-READ from the browser run. - - Four floors, measured across 18 renders. Three fail and one passes, and - all four are pinned: a fix to any one is progress this row must record. + """Core 7 CONFORMS (rendered half), RE-READ from the browser run. + + RETARGETED 2026-09-05 from the VIOLATION pin (work_item_pipeline-96f). The + pin froze three failing floors -- 7 text nodes below 4.5:1, 26 of 34 + interactive controls under 44px on L0, and 16-23 non-text surfaces per + level below 3:1 -- and asserted the fourth (reduced motion) already passed. + All four are now asserted in the REGRESSION direction, over the recorded + run rather than the browser tier's own green (Freeze 3). + + Swept over EVERY recorded render, not one scenario: three floors are + theme- and width-dependent (the light blocks are held in sync only by + comment, and 430px hides controls the wider viewports show), so reading a + single scenario would let seventeen others move unseen. + + The ONE enumerated exemption -- the status-mix donut's backing ring, see + `_probe.NON_TEXT_EXEMPT_CLASSES` -- is asserted here too, by SIZE and by + WHERE it fires, so the allowance cannot quietly grow into the thing that + keeps the non-text arm green. """ - l0 = tier_b("perception.floors", "calm/L0/1280/dark") - l1 = tier_b("perception.floors", "calm/L1/1280/dark") - l1_light = tier_b("perception.floors", "calm/L1/1280/light") - - assert l0["text_below_floor"] == 0, ( - f"OSV1-010 (Core 7): L0 now has {l0['text_below_floor']} text elements below " - f"4.5:1. L0 was the CLEAN level for text contrast -- a regression." - ) - assert l1["text_below_floor"] == 3 and l1_light["text_below_floor"] == 4, ( - f"OSV1-010 (Core 7) PIN MOVED: L1 text below 4.5:1 measured " - f"{l1['text_below_floor']} dark / {l1_light['text_below_floor']} light, " - f"pinned at 3 / 4 (light was 5 before the contrast lane moved " - f"`--ink-quiet`). Movement in either direction means the render changed " - f"-- re-derive (work_item_pipeline-qgo)." - ) - assert l0["controls_below_44px"] == 26 and l0["controls"] == 34, ( - f"OSV1-010 (Core 7) PIN MOVED: L0 measured {l0['controls_below_44px']} of " - f"{l0['controls']} interactive controls under 44px, pinned at 26 of 34 " - f"(35 before the hero rebuild replaced one control)." - ) - assert l0["non_text_below_floor"] > 0, ( - "OSV1-010 (Core 7) PIN BROKE THE RIGHT WAY: every measured control border " - "and icon stroke on L0 now meets 3:1. Re-derive this row." - ) - assert l0["running_animations_under_reduced_motion"] == 0, ( - f"OSV1-010 (Core 7): {l0['running_animations_under_reduced_motion']} " - f"animation(s) now run under `prefers-reduced-motion: reduce`. That floor " - f"PASSED at this measurement -- a regression, and Core 7's kernel-rule half " - f"(OSV1-011) with it." + renders = { + scenario: headline + for scenario, headline in tier_b_summary()["checks"]["perception.floors"].items() + if scenario.startswith("calm/") + } + assert len(renders) == 18, ( + f"OSV1-010 (Core 7): the recorded run sweeps {len(renders)} renders, not the " + f"18 (L0/L1/L2 x 430/900/1280 x dark/light) this clause names. A narrowed " + f"sweep is a narrowed claim -- re-derive." + ) + + text = {s: h["text_below_floor"] for s, h in sorted(renders.items()) if h["text_below_floor"]} + assert not text, ( + f"OSV1-010 (Core 7) REGRESSION, text floor: {text} -- text below 4.5:1 against " + f"its own RENDERED background. Fix the TOKEN and in ALL FOUR declared blocks " + f"(the two light ones are held in sync only by comment); flat pair math " + f"clearing the floor is NOT sufficient here, which is what OSV1-009's honest " + f"limit records and what this row measured." + ) + + targets = { + s: f"{h['controls_below_44px']} of {h['controls']}" + for s, h in sorted(renders.items()) + if h["controls_below_44px"] + } + assert not targets, ( + f"OSV1-010 (Core 7) REGRESSION, target floor: {targets} -- interactive controls " + f"under 44px on their smaller side. The hit area is what has to reach --u; it " + f"need not be the visual size." + ) + + non_text = { + s: f"{h['non_text_below_floor']} of {h['non_text_measured']}" + for s, h in sorted(renders.items()) + if h["non_text_below_floor"] + } + assert not non_text, ( + f"OSV1-010 (Core 7) REGRESSION, non-text floor: {non_text} -- control borders " + f"or icon strokes below 3:1. `--control-edge` is the token that carries an " + f"INTERACTIVE control's boundary; `--glass-hairline`/`-soft` are the " + f"decorative panel edges WCAG 1.4.11 exempts, and swapping one for the other " + f"is how this regresses." + ) + + motion = { + s: h["running_animations_under_reduced_motion"] + for s, h in sorted(renders.items()) + if h["running_animations_under_reduced_motion"] + } + assert not motion, ( + f"OSV1-010 (Core 7) REGRESSION, reduced motion: {motion} animation(s) run " + f"under the preference. This floor passed before the other three were fixed " + f"and must not be traded for them -- see OSV1-011 for the kernel-rule half." + ) + + # The exemption, asserted as a NUMBER and by WHERE it fires. L1 is the only + # level that draws a donut; an exemption appearing on L0 or L2, or a second + # one appearing on L1, means the allowance -- not the fix -- is what is + # keeping the non-text arm green. + exempt = {s: h["non_text_exempt_below_floor"] for s, h in sorted(renders.items())} + stray = {s: n for s, n in exempt.items() if n and "/L1/" not in s} + assert not stray, ( + f"OSV1-010 (Core 7): the non-text exemption fired outside L1: {stray}. It " + f"covers the status-mix donut's backing ring and nothing else -- L0 and L2 " + f"draw no donut, so an entry there is the allowance spreading." + ) + grown = {s: n for s, n in exempt.items() if "/L1/" in s and n != 1} + assert not grown, ( + f"OSV1-010 (Core 7): L1's non-text exemption is pinned at exactly 1 entry " + f"(`.donut-track`, 1.32:1 dark / 1.15:1 light -- the one BLOCKED residual " + f"this row records); observed {grown}. Growth here widens the floor instead " + f"of meeting it." ) + assert contains(WEBTHEME, "--u:44px"), ( "OSV1-010 (Core 7): the 44px target token is gone -- the thing the Tier-B " "bounding-box check exists to verify." ) + assert contains(WEBTHEME, "--control-edge:"), ( + "OSV1-010 (Core 7): `--control-edge` is gone from the token block. It is the " + "token that took every interactive control's border from 1.24-1.60:1 to over " + "3:1 without dragging the decorative panel hairlines along with it." + ) def test_row_osv1_011() -> None: @@ -1937,14 +2004,15 @@ def test_row_osv1_031() -> None: "to CONFORMS and retarget this probe to assert no Core row is red " "(work_item_pipeline-umm)." ) - assert len(red) == 4, ( - f"OSV1-031 (Freeze 5): pinned 4 red Core-carrying rows, observed {len(red)}: " + assert len(red) == 3, ( + f"OSV1-031 (Freeze 5): pinned 3 red Core-carrying rows, observed {len(red)}: " f"{red}. Movement in either direction means this gate's tally changed -- update " f"the pin and the row's notes in the same change. (10 at seed; OSV1-009 went " f"green 2026-09-04, work_item_pipeline-sxh; OSV1-015 and -016 went green " f"2026-09-04, work_item_pipeline-8vv and -dg3; OSV1-001 and OSV1-004 went green " f"2026-09-05, work_item_pipeline-ujy and the Tier-A kit; OSV1-005 went green " - f"2026-09-05, work_item_pipeline-np3.)" + f"2026-09-05, work_item_pipeline-np3; OSV1-010 went green 2026-09-05, " + f"work_item_pipeline-96f.)" ) assert {r["id"] for r in core_rows if r["disposition"] == "NOT-ASSERTABLE"} == { "OSV1-018", diff --git a/ledger/rows.yaml b/ledger/rows.yaml index 67793ea..3aea258 100644 --- a/ledger/rows.yaml +++ b/ledger/rows.yaml @@ -1360,14 +1360,20 @@ webapp.py:1127 flex:{n} 1 0 state-bar segment ratio webapp.py:1823 width:{today_w}px throughput bar, today webapp.py:1826 width:{prior_w}px throughput bar, prior 6 d - webtheme.py:4197 {style} axis ruler numeral offset - webtheme.py:4216 left:{_grad_x(f):.1f}px graduation tick offset - webtheme.py:4223 width:{px}px age bar length + webtheme.py:4279 {style} axis ruler numeral offset + webtheme.py:4298 left:{_grad_x(f):.1f}px graduation tick offset + webtheme.py:4305 width:{px}px age bar length widgets.py:837 width:{pct}% status-mix segment (hatched) widgets.py:839 width:{pct}% status-mix segment Every one is "a bar width, a chart offset" -- the clause's own gloss of what this exemption is FOR. + RE-ANCHORED 2026-09-05 (work_item_pipeline-96f): the three webtheme.py + sites moved +82 lines each (4197/4216/4223 -> 4279/4298/4305) when the + Core 7 rendered-floor fix added token-block comments above them. The + register did not GROW and no site changed -- a line-keyed register moves + whenever anything above it does, which is the cost of keying it that way. + THE ROOT'S TRIAGE RULING WAS APPLIED, and it is what removed 15 sites. Two kinds were on the register that should not have been: (a) A COMPUTED COLOUR. Six sites interpolated a colour rather than a @@ -1595,7 +1601,7 @@ necessary, never sufficient. - id: OSV1-010 - title: rendered contrast, target boxes and a motion trace -- measured, three of four floors fail + title: rendered contrast, target boxes and a motion trace -- measured, all four floors met contract: file: contracts/operator-surface.v1.md clause: Core 7 @@ -1603,14 +1609,120 @@ plus a browser run emitting computed contrast ratios, target bounding boxes, and a motion-preference trace at 430, 900, and 1280px in both themes; the orchestrator re-checks those numbers against the floors. - disposition: VIOLATION - work: work_item_pipeline-qgo + disposition: CONFORMS assertion: kind: probe ref: test_row_osv1_010 notes: > - PINNING ROW -- the probe asserts the CURRENT, KNOWN-WRONG shape on purpose; - a passing probe here is NOT conformance. Flip direction VIOLATION-MOVEMENT. + FLIPPED 2026-09-05, VIOLATION -> CONFORMS (work_item_pipeline-96f). Was a + PINNING row; the probe now asserts the floors are MET, in the regression + direction, and the four Tier-B `xfail(strict)` markers that named this row + are gone from the kit. Every number below is re-read by this row's probe + from `tests/conformance/operator_surface/browser/LAST_RUN.json`, the kit's + committed run summary -- never from the browser tier's own pass/fail + (Freeze 3 / Phase-1 ruling 6), and never transcribed. + + MEASURED IN A REAL BROWSER 2026-09-05 -- chromium 148.0.7778.0 via the + pinned playwright 1.60.0, against a live app on an ephemeral loopback port + over the isolated dolt fixture; 18 renders (L0/L1/L2 x 430/900/1280 x + dark/light), recorded TWICE with byte-identical values on every field this + row asserts. + + ALL FOUR FLOORS, BEFORE -> AFTER: + + TEXT CONTRAST (floor 4.5:1) -- 7 failing nodes -> 0, all 18 renders. + L0 0 of 136 -> 0 of 136 (was already clean; unmoved) + L1 3 of 183 dark, 4 of 183 light -> 0 / 0 + L2 1 of 58 dark, 2 of 58 light -> 0 / 0 + Worst pair moved 3.13:1 -> 4.75:1. Fixed at the TOKEN, in all four + declared blocks: `--ink-quiet` #7c8798 -> #a1a8b5 (dark) and #596473 -> + #4e5764 (light), `--brand-cyan-ink` #0b6b80 -> #0a5e71 (light only; + dark's #22d3ee already read 6.29:1 there). No call site moved off a + token and no reading copy was relabelled decorative. + + This is the HONEST LIMIT OSV1-009 recorded, closed by measurement. Those + same tokens cleared 4.5:1 as flat swatch pairs on the three declared + grounds; in the render they sat on glass-over-glass chip surfaces + (#383a40 dark, #ccd1db light) that flat pair math cannot model -- 3.13:1 + on L1's `.status-chip.st-resolved`, 3.92:1 in light, 3.99:1 on + `.st-held`, 3.77/4.26:1 on L2's drawer count, 4.35:1 on + `.drawer-section label.eyebrow`. Each is now >= 4.75:1 on its own + rendered background. The ink ramp did NOT collapse to buy it: + `--ink-tertiary` reads 7.33:1 dark / 5.75:1 light on the same chip + surface, so quiet stays visibly the quieter step, which is what + OSV1-009's own distinctness assert forbids trading away. + + TARGET SIZE (floor 44px) -- 26 of 34 controls on L0 -> 0 of 34, and 0 of + 41 on L1 / 0 of 20 on L2, at every one of the three widths. + L0 26 of 34 at 900/1280, 16 of 34 at 430 -> 0 + L1 22 of 41 (21 of 40 at 430) -> 0 + L2 11 of 20 -> 0 + The named offenders and what moved: the auto-refresh pause control + (26x26 -> --u), every nav icon-button, the theme toggle and the help + disclosure (34x34 -> --u), the window-range and status tabs (28px tall + -> min-height/min-width --u), the L0 activity-feed rows (37px -> --u), + the L1 items filter `` (181x19, the one field the + base `input[type=text]` rule never reached), the breadcrumb links + (19.5px), the brand wordmark (21px) and the identity links Setup/Logout + (11.5px). For the link-shaped ones the HIT AREA grew and the type did + not: `min-height`/`min-width: var(--u)` on an inline-flex box inside a + row that was already centred, so the 62px header keeps its height. + Inline links stay excluded per WCAG 2.5.8's own inline exception and the + excluded population is still emitted beside the scored one. + + NON-TEXT CONTRAST (floor 3:1) -- 16 of 79 on L0 -> 0, in BOTH themes. + L0 16 of 79 dark, 16 of 79 light -> 0 / 0 + L1 23 of 73 dark, 23 of 73 light -> 0 / 0 (one exemption, below) + L2 11 of 33 dark, 11 of 33 light -> 0 / 0 + Fixed with ONE new token rather than by bumping a hairline: + `--control-edge` (rgba(255,255,255,.40) dark / rgba(11,18,32,.52) + light), applied to the border of an INTERACTIVE control only -- icon + buttons, the pause toggle, KPI cards, fleet/agents/agent/item rows, the + dormant disclosure, active window/status tabs, form fields, the drawer's + action buttons. `--glass-hairline`/`-soft` keep painting every + decorative panel edge unchanged, which is why a rim did not become a + stroke everywhere. Measured worst case after: 3.35:1 dark (#7c7d81 on + #2c2d34), 3.23:1 light (#707682 on #d5d9e3); the icon-button border this + row used to name at 1.27:1 now reads 3.53:1. Neutral by construction (a + white/ink alpha, no hue), so Core 2's status-hue firewall is untouched + and the calm sweep is unmoved. + + REDUCED MOTION -- still PASSES at every level, width and theme: zero + animations run under an emulated `prefers-reduced-motion: reduce` once + the page is quiescent. Unmoved by this change and still asserted here + as well as by OSV1-011. + + HORIZONTAL OVERFLOW -- still PASSES, and still recorded as WEAK + EVIDENCE: `scrollWidth == clientWidth` at all nine level/width pairs, + but `html`/`body` carry `overflow-x: clip` so that metric cannot move. + `elements_beyond_viewport` (border boxes past `clientWidth`, which + clipping cannot hide) is 0 everywhere INCLUDING 430px after the target- + size work -- the check that would have caught a header made too wide by + six 44px buttons. + + THE ONE BLOCKED RESIDUAL, enumerated and machine-checked, never prose: + L1's status-mix donut BACKING RING (`.donut-track`, and its sibling + `.donut-hatch-gap`) measures 1.32:1 dark / 1.15:1 light against its card + and is EXEMPT, via `_probe.NON_TEXT_EXEMPT_CLASSES` -- a frozenset of two + class names, subtracted by `below_non_text_floor`, returned by + `exempt_below_non_text_floor`, counted in every render's headline as + `non_text_exempt_below_floor`, and pinned by its own kit test + (`test_the_non_text_exemption_stays_narrow`: exactly 1 entry on L1, 0 on + L0/L2, and it must still be below the floor or the exemption is deleted). + + WHY BLOCKED rather than fixed, measured at the nearest passing colour + (#616367 dark, #808591 light -- the first step that clears 3:1 against + the card): + track vs. a --blocked arc dark 3.64:1 -> 1.60:1 light 5.88:1 -> 2.25:1 + track vs. an --ink-quiet arc dark 5.73:1 -> 2.52:1 light 5.18:1 -> 1.98:1 + i.e. the EMPTY ring would separate from a real status arc by LESS than the + 3:1 the floor is asking for, and a five-status donut would read as six. + WCAG 1.4.11's graphical-object half covers "parts of graphics required to + understand the content"; the arcs are, the ground they sit on is not. + Meeting the floor here would make the chart say something false, which is + the documented hierarchy this exemption protects. + + HISTORY, left rather than overwritten -- each was true when measured. RE-DERIVED 2026-09-05, GAP -> VIOLATION. The browser run this clause names now exists and emits exactly what the clause asks for -- computed contrast @@ -2686,7 +2798,7 @@ matching bad half go red while every good half stayed green. - id: OSV1-031 title: >- - Freeze 5 -- 4 of the 19 Core-carrying rows are still red, and all four are + Freeze 5 -- 3 of the 19 Core-carrying rows are still red, and all three are now MEASURED VIOLATIONs (no Core row is an unasserted GAP any more) contract: file: contracts/operator-surface.v1.md @@ -2811,6 +2923,28 @@ Four red, every one of them a MEASURED failure. Freeze 5 is met when all four are green and the two NOT-ASSERTABLE rows still name their cadence. + RE-PINNED 2026-09-05 by the rendered-floors lane (work_item_pipeline-96f), + THREE RED, counted by the probe on THIS lane's tree. ONE flip moves it: + OSV1-010 (Core 7, the Tier-B rendered half) VIOLATION -> CONFORMS, and it + is a re-derivation from a real chromium run of this tree, not a row flipped + because a kit file appeared -- the recording was made twice, byte-identical + on every field the row asserts, and its four Tier-B `xfail(strict)` markers + were deleted because they XPASSed. Text below 4.5:1 went 7 nodes -> 0, + controls under 44px 26 of 34 on L0 -> 0, non-text below 3:1 16 of 79 -> 0 + in both themes, with ONE enumerated machine-checked exemption (the donut's + backing ring) recorded in OSV1-010's own notes and counted per render in + `LAST_RUN.json`. + The tally is now: + CONFORMS 14 OSV1-001, -002, -004, -005, -006, -007, -009, -010, + -011, -013, -014, -015, -016, -017 + NOT-ASSERTABLE 2 OSV1-018 (Core 12), OSV1-019 (Core 13) -- unchanged + VIOLATION 3 OSV1-003, -008, -012 + GAP 0 every Core clause is still ASSERTED + Three red, every one of them a MEASURED failure. Freeze 5 is met when all + three are green and the two NOT-ASSERTABLE rows still name their cadence. + This lane counts only its own tree; two other lanes are in flight against + OSV1-003 and OSV1-012, so the orchestrator re-counts on the union. + - id: OSV1-032 title: >- Freeze 6 -- the exemption register is complete and no literal colour, font diff --git a/src/amplifier_work_tracker/webtheme.py b/src/amplifier_work_tracker/webtheme.py index ef8275c..046c0e7 100644 --- a/src/amplifier_work_tracker/webtheme.py +++ b/src/amplifier_work_tracker/webtheme.py @@ -130,6 +130,26 @@ --glass-blur-strong:40px; --glass-hairline:rgba(255,255,255,.14); --glass-hairline-soft:rgba(255,255,255,.08); + /* --control-edge (OSV1-010, Core 7 non-text floor): the visual BOUNDARY of + an INTERACTIVE control -- WCAG 1.4.11's own scope -- as distinct from a + decorative panel hairline, which the guideline exempts and which + --glass-hairline/-soft keep painting unchanged. + + MEASURED, and the reason a second token exists rather than a bumped + hairline: at .08-.14 alpha every control border on this surface computed + 1.24-1.60:1 against its own resolved background (icon-btn #303238 on + #1e2027 = 1.27:1; item/fleet/agent rows #27292f on #14161d = 1.24:1), + i.e. under half the 3:1 floor. Raising --glass-hairline itself to clear + that floor would drag every non-interactive panel edge along with it and + turn a glass rim into a hard stroke. + + .40 white is the lowest step that clears 3:1 on EVERY dark ground this + surface composites a control onto -- #05070f 3.46:1, #14161d 3.59:1, + #1e2027 3.53:1, #22242b 3.47:1, #2c2d34 3.35:1 -- margin for chromium's + own rounding, no more weight than the floor requires. Neutral by + construction (a white/ink alpha, no hue), so Core 2's status-hue + firewall is untouched. Keep in sync with the two light blocks below. */ + --control-edge:rgba(255,255,255,.40); --glass-shadow:0 8px 32px rgba(2,6,15,.45),inset 0 1px 0 rgba(255,255,255,.06); --glass-shadow-float:0 24px 64px rgba(2,6,15,.55),inset 0 1px 0 rgba(255,255,255,.08); @@ -157,7 +177,22 @@ symmetric darker bump). Keep this value in sync with the two other dark-mode blocks below (base :root here and :root[data-theme="dark"]). */ --ink-tertiary:#c8d0de; - --ink-quiet:#7c8798; + /* --ink-quiet brightened from #7c8798 (OSV1-010, Core 7's RENDERED half -- + the honest limit OSV1-009 recorded and could not close by flat token + math). Measured in chromium against the real composited surfaces this + ink actually paints on, not against a bare ground: 3.13:1 on L1's + `.status-chip.st-resolved` (chip glass over row glass over panel glass + = #383a40) and 3.77:1 on L2's `.actions-drawer summary .count` + (#2c2d34). Both cleared 4.5:1 as a flat swatch pair; neither did in the + render, which is exactly the glass-over-gradient lift the token block's + --ink-tertiary comment above already documents. + + #a1a8b5 takes the worst rendered pair to 4.75:1 along the same slate + hue. NOT taken further: --ink-tertiary reads 7.33:1 on that same chip + surface, so quiet stays visibly the quieter of the two -- the ink ramp + is not bought flat, which OSV1-009's own distinctness assert forbids. + Keep in sync with `:root[data-theme="dark"]` below. */ + --ink-quiet:#a1a8b5; --ink-on-solid:#f8fafc; --ink-on-ground-inverse:#05070f; @@ -314,6 +349,16 @@ hairline-bordered surface in one place rather than per-component. */ --glass-hairline:rgba(11,18,32,.22); --glass-hairline-soft:rgba(11,18,32,.16); + /* --control-edge, light half (OSV1-010). Same job as the dark block's: + an INTERACTIVE control's boundary, never a decorative panel hairline. + .52 ink is the lowest step clearing 3:1 on every light ground a + control composites onto -- #eef2fb 3.55:1, #e4e8f2 3.47:1, #dee2ec + 3.43:1, #dbdfe9 3.42:1, #d5d9e3 3.32:1. Higher than the dark block's + .40 because an ink alpha over a light ground washes out faster than a + white alpha over near-black -- the same asymmetry --glass-hairline + already records above. Keep in sync with `:root[data-theme="light"]` + below; they are held together only by comment. */ + --control-edge:rgba(11,18,32,.52); --glass-shadow:0 8px 32px rgba(15,23,42,.10),inset 0 1px 0 rgba(255,255,255,.6); --glass-shadow-float:0 24px 64px rgba(15,23,42,.16),inset 0 1px 0 rgba(255,255,255,.7); --ink-primary:#0b1220; @@ -333,8 +378,17 @@ not. Darkened along the SAME hue to 5.36/5.08/4.71:1. NOT taken to 7:1: quiet must stay visibly quieter than --ink-tertiary (7.85:1). Dark mode already cleared (5.53/5.26/5.67:1), unchanged. Keep in sync with - `:root[data-theme="light"]` below. */ - --ink-quiet:#596473; + `:root[data-theme="light"]` below. + + DARKENED AGAIN to #4e5764 (OSV1-010, Core 7's RENDERED half). #596473 + cleared 4.71:1 as a flat swatch on the darkest declared ground, and + still measured 3.92:1 in chromium on L1's `.status-chip.st-resolved` + (#ccd1db, chip glass over row glass over panel glass) and 4.26:1 on + L2's drawer count (#d5d9e3) -- the glass-over-gradient lift this file + documents at --ink-tertiary, now measured in the render rather than + modelled. #4e5764 takes the worst rendered pair to 4.78:1 on the same + hue. Still NOT taken to --ink-tertiary, which reads 5.75:1 there. */ + --ink-quiet:#4e5764; --ink-on-ground-inverse:#f8fafc; /* reserved status hues re-tuned darker so text/icons still clear 4.5:1 on a light ground */ --alarm:#92400e; @@ -361,7 +415,15 @@ --watch:#3a4468; --watch-surface:rgba(154,168,204,.16); --watch-ink-on-surface:#232c4a; - --brand-cyan-ink:#0b6b80; + /* --brand-cyan-ink darkened from #0b6b80 (OSV1-010, Core 7's rendered + half). It clears 4.80:1 as a flat pair on the base light ground, and + measured 3.99:1 in chromium on L1's `.status-chip.st-held` (#ccd1db) + and 4.35:1 on L2's `.drawer-section label.eyebrow` (#d5d9e3) -- both + real reading copy, not chrome. #0a5e71 is the same hue one step + darker: 4.81:1 on the worst of those. Dark mode's #22d3ee already + reads 6.29:1 on the same chip surface and is unchanged. Keep in sync + with `:root[data-theme="light"]` below. */ + --brand-cyan-ink:#0a5e71; --brand-purple-ink:#7e22ce; } /* -- light-mode COMPONENT overrides (not pure token re-tuning) --------- @@ -399,12 +461,13 @@ --glass-fill-row-selected:rgba(8,145,178,.10); --glass-hairline:rgba(11,18,32,.22); --glass-hairline-soft:rgba(11,18,32,.16); + --control-edge:rgba(11,18,32,.52); /* keep in sync with the light media block above */ --glass-shadow:0 8px 32px rgba(15,23,42,.10),inset 0 1px 0 rgba(255,255,255,.6); --glass-shadow-float:0 24px 64px rgba(15,23,42,.16),inset 0 1px 0 rgba(255,255,255,.7); --ink-primary:#0b1220; --ink-secondary:#33415a; --ink-tertiary:#3d4b63; /* keep in sync with the light-mode media block above */ - --ink-quiet:#596473; /* likewise -- OSV1-009: 2.72:1 -> 4.71:1 worst case */ + --ink-quiet:#4e5764; /* likewise -- OSV1-010: 3.92:1 -> 4.78:1 rendered worst case */ --ink-on-ground-inverse:#f8fafc; --alarm:#92400e; --alarm-surface:rgba(245,158,11,.08); @@ -415,7 +478,7 @@ --watch:#3a4468; --watch-surface:rgba(154,168,204,.16); --watch-ink-on-surface:#232c4a; - --brand-cyan-ink:#0b6b80; + --brand-cyan-ink:#0a5e71; /* likewise -- OSV1-010: 3.99:1 -> 4.81:1 rendered worst case */ --brand-purple-ink:#7e22ce; } @@ -455,12 +518,13 @@ --glass-fill-row-selected:rgba(34,211,238,.10); --glass-hairline:rgba(255,255,255,.14); --glass-hairline-soft:rgba(255,255,255,.08); + --control-edge:rgba(255,255,255,.40); /* keep in sync with the base :root above */ --glass-shadow:0 8px 32px rgba(2,6,15,.45),inset 0 1px 0 rgba(255,255,255,.06); --glass-shadow-float:0 24px 64px rgba(2,6,15,.55),inset 0 1px 0 rgba(255,255,255,.08); --ink-primary:#f8fafc; --ink-secondary:#d6dee8; --ink-tertiary:#c8d0de; /* keep in sync with the base :root block above */ - --ink-quiet:#7c8798; + --ink-quiet:#a1a8b5; /* likewise -- OSV1-010: 3.13:1 -> 4.75:1 rendered worst case */ --ink-on-ground-inverse:#05070f; --alarm:#f59e0b; --alarm-surface:rgba(245,158,11,.14); @@ -572,7 +636,7 @@ independently-rounded ones. */ .top .brand{font-family:var(--sans);font-size:19px;font-weight:700; letter-spacing:-.01em;color:var(--ink);text-decoration:none;line-height:1; - display:flex;align-items:center;gap:9px} + display:flex;align-items:center;gap:9px;min-height:var(--u)} /* the brand mark -- a small squircle carrying the rim gradient, the ONE place besides the wordmark this gradient is allowed near identity chrome. */ .top .brand .bm{width:9px;height:9px;border-radius:3px; @@ -602,7 +666,13 @@ .top .identity{font-family:var(--sans);font-size:11.5px;color:var(--dim); letter-spacing:.02em;display:flex;align-items:center;gap:9px;white-space:nowrap; line-height:1} -.top .identity a{color:var(--mid);text-decoration:none} +/* Core 7 target floor (OSV1-010): these read 31.2x11.5 and 36.56x11.5 -- + an 11px glyph box with no padding at all. The HIT AREA grows to --u in + both axes; the type keeps its size and the header keeps its 62px height, + so this is padding, not a bigger-looking control. */ +.top .identity a{color:var(--mid);text-decoration:none; + display:inline-flex;align-items:center;justify-content:center; + min-height:var(--u);min-width:var(--u)} .top .identity a:hover{color:var(--brand-cyan-ink)} .dot{width:6px;height:6px;border-radius:50%;flex:0 0 6px;display:inline-block} /* the live/"healthy" pulse is GOOD NEWS -> neutral (--live), never the accent. @@ -1634,7 +1704,7 @@ class lives on `` itself, which survives the auto-refresh body-swap input[type=text],input[type=password],textarea,select{ display:block;width:100%;max-width:480px;padding:0.55rem 0.7rem;box-sizing:border-box; font-family:var(--sans);font-size:13.5px;min-height:var(--u); - border:1px solid var(--rule);border-radius:var(--radius-sm);background:var(--raise); + border:1px solid var(--control-edge);border-radius:var(--radius-sm);background:var(--raise); color:var(--ink); } input[type=text]:focus,input[type=password]:focus,textarea:focus,select:focus{ @@ -2345,8 +2415,9 @@ class name, or test needs to change. color:var(--ink-tertiary); } .wt-observatory .icon-btn{ - width:34px;height:34px;border-radius:var(--radius-sm);display:inline-flex;align-items:center; - justify-content:center;background:var(--glass-fill);border:1px solid var(--glass-hairline-soft); + width:var(--u);height:var(--u);border-radius:var(--radius-sm);display:inline-flex; + align-items:center; + justify-content:center;background:var(--glass-fill);border:1px solid var(--control-edge); color:var(--ink-tertiary);cursor:pointer;flex-shrink:0;text-decoration:none; /* `padding:0` + `font:inherit` (visual-polish punchlist item 5): the nav's search action is a real `