Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:45:34.942Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

collect-omega-installed-observations

2143 lines · 91.0 KB · text
1#!/usr/bin/env python3
2"""Collect the installed Omega observations the RC proof requires.
3
4Eleven observations: three manual-journey checks and eight accessibility
5checks. Each one is *performed* here, not asserted. A check that cannot be
6performed on this host records `blocked` and the run refuses, because an
7observation nobody made is worse than a missing one — it looks like evidence.
8
9There is exactly one alternative to performing a check, and it is not a pass.
10A check the owner has directed us not to build for may record `waived`, which
11carries the owner's own words, the date of the direction, and the upstream
12parity the direction is conditioned on. `waived` never counts as `passed`, it
13never rolls up into a green status, and the report status says
14`passed_with_waivers` so nobody can read the summary and believe the
15observation happened. `WAIVABLE_CHECKS` in `omega_identity_evidence` keeps that
16door as narrow as the thing that is genuinely unobservable: a 360-pixel
17viewport and a larger UI font are ordinary rendering conditions and are
18performed here, not waived.
19
20Two checks depend on a system flag only the owner can set: increased contrast
21and reduced motion live in a domain `defaults write` is refused on. The flag is
22their precondition and never their observation. With increased contrast on, the
23window is read for legible text and the focus indicator is moved and watched;
24with reduced motion on, a journey is completed by keyboard and undone by menu,
25and every state it passes through is read from a still frame. Recording either
26as a pass because a preference is set would state a fact about the host and
27call it a fact about the product.
28
29The screen-reader check reads the macOS accessibility tree through
30`AXUIElement`, which is the exact surface a screen reader consumes. That is a
31real observation of what assistive technology would receive. It is not a claim
32that a person listened to speech, and the recorded `assistive_technology` names
33the interface used so nobody can mistake it for one.
34
35Usage:
36    collect-omega-installed-observations \\
37        --app /Applications/Omega.app --candidate-evidence <path> \\
38        --evidence-root <dir> --output <path>
39"""
40
41from __future__ import annotations
42
43import argparse
44import hashlib
45import json
46import os
47import re
48import subprocess
49import sys
50import time
51from datetime import datetime, timezone
52from pathlib import Path
53from typing import Any
54
55sys.path.insert(0, str(Path(__file__).resolve().parent))
56
57from omega_identity_evidence import (  # noqa: E402
58    ACCESSIBILITY_CHECKS,
59    INSTALLED_OBSERVATIONS_SCHEMA,
60    MANUAL_JOURNEY_CHECKS,
61    WAIVABLE_CHECKS,
62    IdentityEvidenceError,
63    validate_observation_facts,
64)
65
66OBSERVATIONS_SCHEMA = INSTALLED_OBSERVATIONS_SCHEMA
67
68MANUAL_JOURNEY = sorted(MANUAL_JOURNEY_CHECKS)
69ACCESSIBILITY = sorted(ACCESSIBILITY_CHECKS)
70
71ZED_DATA = Path.home() / "Library" / "Application Support" / "Zed"
72
73#: The application process name as macOS reports it to System Events.
74PROCESS = "omega"
75
76#: The exact process this run observes, set from `--pid`.
77#:
78#: A name is not an identity. This host routinely runs the installed candidate,
79#: a second copy under an emulated profile, and a development build, and macOS
80#: reports all three as `omega`. `tell process "omega"` addresses whichever one
81#: System Events happens to return first, so a run that targets by name can
82#: capture one application while it drives another and never notice. Every
83#: System Events call below addresses this identifier when it is set.
84PROCESS_PID: int | None = None
85
86#: The window-server identifier of the window this run observes, set from
87#: `--window-id`.
88#:
89#: `screencapture -l` captures that window even when another application holds
90#: the active space, which is what makes an observation possible on a shared
91#: host without taking the screen away from whoever is using it.
92WINDOW_ID: int | None = None
93
94#: Path to the compiled `script/omega-window-ocr.swift`, set from `--ocr`.
95#:
96#: This reads the text a person actually sees, with the position of each line.
97#: It is how the two journey checks below are performed on an application that
98#: publishes no accessibility tree, and it is a stronger observation than the
99#: tree read it replaces: a label present in a tree but clipped off the screen
100#: passes an accessibility-tree read and fails this one.
101OCR_TOOL: Path | None = None
102
103#: Path to the compiled `script/omega-window-list.swift`, set from
104#: `--window-list`. Used to read the window frame that the accessibility tree
105#: does not publish.
106WINDOW_LIST_TOOL: Path | None = None
107
108#: Width in points that the 360-pixel viewport check drives the window to.
109NARROW_VIEWPORT_POINTS = 360
110
111#: `ui_font_size` the larger-UI-font check writes into the installed
112#: application's own settings file. The validator refuses anything under 18.
113LARGE_UI_FONT_PIXELS = 24
114
115#: A forward focus move has to repaint at least this many pixels before it
116#: counts as a visible indicator moving rather than a caret blinking.
117FOCUS_MOVE_MINIMUM_PIXELS = 2000
118
119#: A reverse move has to leave the window at least this many times closer to
120#: the earlier image than a forward move moved it away.
121FOCUS_RETURN_RATIO = 10
122
123#: How many lines of text have to be recognised off the rendered window before
124#: the increased-contrast render counts as legible, and the confidence below
125#: which a recognised line is not counted. Legibility is read from the product
126#: a person sees, not from a layout tree, so a render that is present but
127#: unreadable fails here.
128CONTRAST_LEGIBLE_LINES = 12
129CONTRAST_LEGIBLE_CONFIDENCE = 0.5
130
131#: How many lines of text have to be recognised off the rendered window in an
132#: appearance before that appearance counts as legible, and the confidence
133#: below which a recognised line is not counted. `content_legible` is a claim
134#: about what a person can read in that appearance, so it is read off the
135#: pixels exactly the way the increased-contrast claim is. It used to be a
136#: literal `True`.
137APPEARANCE_LEGIBLE_LINES = 12
138APPEARANCE_LEGIBLE_CONFIDENCE = 0.5
139
140#: How many pixels the light capture and the dark capture have to differ by
141#: before the pair counts as two appearances rather than one. A window that
142#: ignored the host, and a frozen or blank one, return nearly the same image
143#: twice and cannot clear this; a theme change repaints the background of every
144#: pane and clears it by orders of magnitude. Without this the two checks can
145#: be satisfied by a single unchanging picture taken twice.
146APPEARANCE_DIFFERENCE_PIXELS = 50_000
147
148#: How long the reduced-motion journey waits after each input before calling a
149#: state settled, and by how much the frame taken as soon as the harness could
150#: take one may differ from the settled frame. Stated openly: a state that is
151#: still moving when the person arrives differs between the two captures by a
152#: sizeable fraction of the change the step made, and fails.
153MOTION_SETTLED_SECONDS = 2.0
154MOTION_SETTLE_RATIO = 10
155
156#: The command the reduced-motion journey completes, the text that has to be
157#: legible on screen before it is completed, and the View-menu item that undoes
158#: it. The command is chosen because the candidate offers the same action by two
159#: independent routes, which is what lets the journey be completed by keyboard
160#: and checked by menu.
161MOTION_JOURNEY_COMMAND = "toggle right dock"
162MOTION_JOURNEY_MATCH = "workspace: toggle right dock"
163MOTION_JOURNEY_MENU_ITEM = "Toggle Right Dock"
164
165#: What a run says when an owner-only accessibility flag is off. `defaults
166#: write` cannot set these: macOS answers `Could not write domain`, so the
167#: window in which they can be observed exists only while a person has turned
168#: them on.
169OWNER_FLAG_OFF = (
170    "{check}: system flag {key} is off on this host. macOS privacy policy "
171    "protects that domain, so only the owner can turn it on, in System "
172    "Settings > Accessibility > Display."
173)
174
175#: Anything matching these in an accessibility value would be a secret reaching
176#: assistive technology, which is the failure the screen-reader check exists to
177#: detect.
178SECRET_SHAPES = [re.compile(p) for p in (r"nsec1", r"ncryptsec1", r"\b[0-9a-f]{64}\b")]
179
180#: Owner direction of 2026-07-25, quoted exactly as given. It is recorded
181#: verbatim, capitals and typing errors included, because a paraphrase of an
182#: owner direction is not the direction.
183OWNER_ACCESSIBILITY_DIRECTION = (
184    "IF ZED DOESNT HAVE IT WIRED UP, UYOU SHOULD NOT EXPECT US TO HAVE IT -- "
185    "REMOVE/CLOSE THAT ISSUE AS WONTDO SO WE CAN DO IT LATER, WE ARE NOT "
186    "ADDING ASSISTIVE TECHNOLOGY AT THIS TIME IF ZED DOESNT HAVE IT"
187)
188OWNER_DIRECTION_DATE = "2026-07-25T00:00:00Z"
189OWNER_DIRECTION_ISSUE = "https://github.com/OpenAgentsInc/omega/issues/71"
190
191#: Written by `observe-upstream-accessibility-parity`. The owner direction only
192#: permits an omission where Zed omits it too, so the parity has to be
193#: observed, never assumed.
194UPSTREAM_PARITY_SCHEMA = "openagents.omega.upstream-accessibility-parity.v1"
195
196#: macOS supplies these window-frame controls itself. A window whose only
197#: direct children are these publishes no application accessibility tree.
198WINDOW_FRAME_CONTROLS = {"close button", "full screen button", "minimize button"}
199
200
201class ObservationError(Exception):
202    pass
203
204
205def now() -> str:
206    return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
207
208
209def sha256_bytes(data: bytes) -> str:
210    return hashlib.sha256(data).hexdigest()
211
212
213def tree_digest(root: Path) -> str:
214    digest = hashlib.sha256()
215    for current, dirs, files in os.walk(root, followlinks=False):
216        dirs.sort()
217        files.sort()
218        for name in files:
219            path = Path(current) / name
220            try:
221                if path.is_symlink() or not path.is_file():
222                    continue
223                digest.update(str(path.relative_to(root)).encode())
224                digest.update(hashlib.sha256(path.read_bytes()).digest())
225            except OSError:
226                continue
227    return digest.hexdigest()
228
229
230def osascript(script: str, timeout: int = 60) -> tuple[bool, str]:
231    try:
232        result = subprocess.run(
233            ["osascript", "-e", script], capture_output=True, text=True, timeout=timeout
234        )
235    except (OSError, subprocess.TimeoutExpired):
236        return False, ""
237    return result.returncode == 0, result.stdout.strip()
238
239
240def target() -> str:
241    """The System Events clause that addresses the application under test.
242
243    With `--pid` this is the one process with that identifier. Without it the
244    clause falls back to the process name, which is only safe on a host running
245    a single copy.
246    """
247    if PROCESS_PID is not None:
248        return f'(first process whose unix id is {PROCESS_PID})'
249    return f'process "{PROCESS}"'
250
251
252def run_tool(tool: Path, *arguments: str) -> str | None:
253    try:
254        result = subprocess.run(
255            [str(tool), *arguments], capture_output=True, text=True, timeout=120
256        )
257    except (OSError, subprocess.SubprocessError):
258        return None
259    if result.returncode != 0:
260        return None
261    return result.stdout
262
263
264def candidate_windows() -> list[dict[str, Any]]:
265    """Every window the window server attributes to the process under test."""
266    if WINDOW_LIST_TOOL is None or PROCESS_PID is None:
267        return []
268    output = run_tool(WINDOW_LIST_TOOL)
269    if output is None:
270        return []
271    try:
272        windows = json.loads(output)
273    except json.JSONDecodeError:
274        return []
275    return [
276        window
277        for window in windows
278        if window.get("pid") == PROCESS_PID and window.get("layer") == 0
279    ]
280
281
282#: Smallest window the visibility check will accept as the main window, in
283#: points. A GPUI application also owns menu-bar-height and offscreen helper
284#: windows, and capturing one of those instead of the product would be a
285#: capture of nothing.
286MAIN_WINDOW_MINIMUM_HEIGHT = 400
287
288
289def visible_window() -> tuple[int, int, int, int] | None:
290    """Frame of the candidate's main window, only while it is on screen.
291
292    Returns `None` when the window server says the window is not being
293    displayed. Every caller treats that as a capture that could not be made,
294    which is never a pass.
295    """
296    best: tuple[int, int, int, int] | None = None
297    best_area = 0
298    for window in candidate_windows():
299        if not window.get("onscreen"):
300            continue
301        bounds = window.get("bounds")
302        if not isinstance(bounds, dict):
303            continue
304        try:
305            left = int(bounds["X"])
306            top = int(bounds["Y"])
307            width = int(bounds["Width"])
308            height = int(bounds["Height"])
309        except (KeyError, TypeError, ValueError):
310            continue
311        if height < MAIN_WINDOW_MINIMUM_HEIGHT:
312            continue
313        if width * height > best_area:
314            best_area = width * height
315            best = (left, top, width, height)
316    return best
317
318
319def raise_window() -> None:
320    """Ask the candidate's own window to come to the top of the stack."""
321    osascript(
322        f"tell application \"System Events\" to tell {target()} "
323        'to perform action "AXRaise" of window 1'
324    )
325
326
327def ocr_lines(path: Path) -> list[dict[str, Any]] | None:
328    """The text of a capture, each line with its position down the window.
329
330    `None` means the read could not be made, which never becomes a pass.
331    """
332    if OCR_TOOL is None:
333        return None
334    output = run_tool(OCR_TOOL, str(path))
335    if output is None:
336        return None
337    try:
338        return json.loads(output).get("lines")
339    except (json.JSONDecodeError, AttributeError):
340        return None
341
342
343def confident_lines(
344    lines: list[dict[str, Any]] | None, minimum_confidence: float
345) -> list[dict[str, Any]] | None:
346    """The recognised lines a reader would actually be able to read.
347
348    `None` in means the read could not be made and `None` comes back out; a
349    read that could not be made never becomes a pass.
350    """
351    if lines is None:
352        return None
353    return [
354        line
355        for line in lines
356        if isinstance(line.get("text"), str)
357        and line["text"].strip()
358        and float(line.get("confidence", 0.0)) >= minimum_confidence
359    ]
360
361
362def appearance_observed(
363    name: str,
364    lines: list[dict[str, Any]] | None,
365    difference_pixels: int | None,
366) -> tuple[bool, str]:
367    """Whether one appearance was observed on the product, and why not if not.
368
369    Two facts, both read off the pixels. The window has to be legible in this
370    appearance, and the two appearances have to be different pictures. The host
371    being in the appearance is the precondition of the observation and is never
372    the observation: a frozen, blank, or appearance-ignoring window satisfies
373    the host and fails here.
374    """
375    if lines is None:
376        return False, (
377            f"{name}-theme: the rendered window could not be read, so its "
378            "legibility in this appearance was not observed"
379        )
380    if len(lines) < APPEARANCE_LEGIBLE_LINES:
381        return False, (
382            f"{name}-theme: legible_lines={len(lines)} "
383            f"(minimum {APPEARANCE_LEGIBLE_LINES})"
384        )
385    if difference_pixels is None:
386        return False, (
387            f"{name}-theme: the light and dark captures could not be compared, "
388            "so the window was not shown to follow the host appearance"
389        )
390    if difference_pixels < APPEARANCE_DIFFERENCE_PIXELS:
391        return False, (
392            f"{name}-theme: the light and dark captures differ by "
393            f"{difference_pixels} pixels (minimum {APPEARANCE_DIFFERENCE_PIXELS}), "
394            "so the window did not follow the host appearance"
395        )
396    return True, ""
397
398
399def first_position(lines: list[dict[str, Any]], phrase: str) -> float | None:
400    """How far down the window a phrase first appears, or `None` if absent."""
401    needle = phrase.casefold()
402    for line in lines:
403        text = line.get("text")
404        if isinstance(text, str) and needle in text.casefold():
405            return float(line.get("y", 1.0))
406    return None
407
408
409def screencapture(path: Path, region: tuple[int, int, int, int] | None = None) -> bool:
410    command = ["screencapture", "-x", "-o"]
411    if WINDOW_ID is not None:
412        # Capture the window itself. A region capture reads whatever the screen
413        # is showing, which on a shared host is whatever the person at the
414        # keyboard is doing, not the candidate.
415        command += ["-l", str(WINDOW_ID)]
416    elif region is not None:
417        command += ["-R", ",".join(str(value) for value in region)]
418    command.append(str(path))
419    try:
420        subprocess.run(command, check=True, timeout=60)
421    except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
422        return False
423    return path.is_file()
424
425
426#: Who performed this run, and in which emulated profile. Written into every
427#: piece of evidence, because a reader has to be able to tell an
428#: agent-performed journey from an owner observation without being told. The
429#: owner grant of 2026-07-25 permits an agent to perform these journeys. It
430#: does not permit recording one as an owner observation.
431PERFORMED_BY = "agent"
432PROFILE: str | None = None
433
434
435def write_evidence(root: Path, name: str, payload: dict[str, Any]) -> dict[str, str]:
436    path = root / f"observation-{name}.json"
437    body = dict(payload)
438    body["performed_by"] = PERFORMED_BY
439    if PROFILE is not None:
440        body["emulated_profile"] = PROFILE
441    path.write_text(json.dumps(body, indent=2, sort_keys=True) + "\n", encoding="utf-8")
442    return {"path": path.name, "sha256": sha256_bytes(path.read_bytes())}
443
444
445def attach_screenshot(
446    root: Path, name: str, region: tuple[int, int, int, int] | None = None
447) -> dict[str, str] | None:
448    """Capture the candidate, or capture nothing.
449
450    A region capture reads the screen, not a window. If the candidate is not in
451    front, the rectangle where its window sits is filled by whatever is, and
452    the capture is then evidence about another application. That is the same
453    class of error as sending a keystroke to whatever happens to be frontmost,
454    and it is refused the same way: the candidate is brought to the front and
455    the identifier is checked before the shutter, and a capture that cannot be
456    established as the candidate's is not written.
457    """
458    if PROCESS_PID is not None and WINDOW_ID is None:
459        if not activate():
460            return None
461        # Frontmost is not the same as visible. An application can hold the
462        # front while its window sits on a space that is not the one being
463        # displayed, and a region capture then reads the windows that are.
464        # Ask the window server whether this window is actually on screen.
465        visible = visible_window()
466        if visible is None:
467            return None
468        region = visible
469    path = root / f"screenshot-{name}.png"
470    if not screencapture(path, region):
471        return None
472    return {"path": path.name, "sha256": sha256_bytes(path.read_bytes())}
473
474
475def window_frame() -> tuple[int, int, int, int] | None:
476    """Position and size of the candidate's front window, in points.
477
478    The accessibility tree is asked first. A GPUI application publishes none,
479    and on such an application `window 1` does not exist even though the window
480    is on the screen, so the window server is asked second.
481    """
482    ok, output = osascript(
483        f"tell application \"System Events\" to tell {target()} "
484        "to get {position, size} of window 1"
485    )
486    if ok:
487        parts = [part.strip() for part in output.split(",")]
488        if len(parts) == 4:
489            try:
490                left, top, width, height = (int(part) for part in parts)
491                return left, top, width, height
492            except ValueError:
493                pass
494    for window in candidate_windows():
495        if WINDOW_ID is not None and window.get("id") != WINDOW_ID:
496            continue
497        bounds = window.get("bounds")
498        if not isinstance(bounds, dict):
499            continue
500        try:
501            return (
502                int(bounds["X"]),
503                int(bounds["Y"]),
504                int(bounds["Width"]),
505                int(bounds["Height"]),
506            )
507        except (KeyError, TypeError, ValueError):
508            continue
509    return None
510
511
512def set_window_size(width: int, height: int) -> bool:
513    ok, _ = osascript(
514        f"tell application \"System Events\" to tell {target()} "
515        f"to set size of window 1 to {{{width}, {height}}}"
516    )
517    return ok
518
519
520def frontmost_process() -> str:
521    ok, output = osascript(
522        'tell application "System Events" to get name of first process whose frontmost is true'
523    )
524    return output.strip() if ok else ""
525
526
527def frontmost_pid() -> int | None:
528    ok, output = osascript(
529        "tell application \"System Events\" to get unix id of "
530        "(first process whose frontmost is true)"
531    )
532    if not ok:
533        return None
534    try:
535        return int(output.strip())
536    except ValueError:
537        return None
538
539
540#: How many times to ask for the front before reporting that the candidate did
541#: not get it. A shared host has other work on it, and another application
542#: taking the front for a moment is ordinary contention rather than a fact
543#: about the candidate. Retrying changes nothing about what is accepted: the
544#: identifier is still checked after every attempt, and a run that never wins
545#: the front still refuses.
546ACTIVATE_ATTEMPTS = 6
547
548
549def is_frontmost() -> bool:
550    if PROCESS_PID is not None:
551        return frontmost_pid() == PROCESS_PID
552    return frontmost_process().lower() == PROCESS.lower()
553
554
555def activate() -> bool:
556    """Bring the application under test to the front, or report that it is not.
557
558    With `--pid` the check is on the identifier, not the name. Three processes
559    on this host answer to `omega`, and a name comparison would report success
560    while a different copy held the keyboard.
561    """
562    for attempt in range(ACTIVATE_ATTEMPTS):
563        osascript(
564            f"tell application \"System Events\" to tell {target()} "
565            "to set frontmost to true"
566        )
567        raise_window()
568        time.sleep(2 if attempt == 0 else 1.5)
569        if is_frontmost() and (WINDOW_LIST_TOOL is None or visible_window() is not None):
570            return True
571    return False
572
573
574def keystroke(script_fragment: str) -> bool:
575    """Send one keystroke to the candidate, or refuse.
576
577    macOS routes synthesized keys to whichever application is frontmost, not to
578    the process named in the AppleScript. An unchecked keystroke therefore
579    lands in whatever window happens to be in front, which during an earlier
580    pass of this harness opened a Page Setup sheet in the operator's terminal
581    and was very nearly recorded as the candidate responding to the keyboard.
582    Every keystroke re-activates the candidate first and refuses if it is not
583    in front, so a keyboard observation can only ever be an observation of this
584    application.
585    """
586    if not activate():
587        return False
588    ok, _ = osascript(f'tell application "System Events" to {script_fragment}')
589    time.sleep(1.5)
590    return ok
591
592
593def window_digest(root: Path, name: str) -> tuple[str | None, dict[str, str] | None]:
594    """Capture the candidate's window and return its pixel digest."""
595    frame = window_frame()
596    if frame is None:
597        return None, None
598    reference = attach_screenshot(root, name, frame)
599    if reference is None:
600        return None, None
601    return reference["sha256"], reference
602
603
604#: `entire contents` value recorded when the process is running and its window
605#: is on the screen, and the accessibility tree publishes no window at all.
606#: This is a weaker publication than an empty tree, not a failure to look: the
607#: window server is asked separately and confirms the window exists.
608NO_WINDOW_IN_TREE = -2
609
610
611def ax_children(process: str) -> tuple[bool, int, list[str]]:
612    """Direct children of the front window, as assistive technology sees them.
613
614    Returns whether the process was reachable, the size of `entire contents`,
615    and the role description of each direct child. macOS supplies the window
616    frame buttons itself, so a window whose only children are those buttons and
617    whose `entire contents` is empty publishes no application accessibility
618    tree at all.
619
620    A running process whose window the tree does not list at all reports
621    `NO_WINDOW_IN_TREE`, which is reachable and publishes nothing. It is
622    distinguished from an absent process because those two states must not be
623    recorded the same way: one is a fact about the product, and the other is a
624    fact about the harness.
625    """
626    clause = target() if PROCESS_PID is not None else f'process "{process}"'
627    exists_clause = (
628        f"(exists {clause})"
629        if PROCESS_PID is not None
630        else f'(exists process "{process}")'
631    )
632    script = f"""
633    tell application "System Events"
634        if not {exists_clause} then return "NOPROC"
635        tell {clause}
636            if (count of windows) is 0 then return "NOWINDOW"
637            set w to window 1
638            set out to ""
639            try
640                set out to out & "contents=" & (count of (entire contents of w)) & linefeed
641            on error
642                set out to out & "contents=-1" & linefeed
643            end try
644            try
645                repeat with e in UI elements of w
646                    set out to out & "child=" & (role description of e) & linefeed
647                end repeat
648            end try
649            return out
650        end tell
651    end tell
652    """
653    ok, output = osascript(script, timeout=90)
654    if ok and output == "NOWINDOW" and candidate_windows():
655        # The process is running and the window server is showing its window.
656        # The tree simply does not carry it.
657        return True, NO_WINDOW_IN_TREE, []
658    if not ok or output in {"NOPROC", "NOWINDOW"}:
659        return False, -1, []
660    contents = -1
661    children: list[str] = []
662    for line in output.split("\n"):
663        line = line.strip()
664        if line.startswith("contents="):
665            try:
666                contents = int(line.split("=", 1)[1])
667            except ValueError:
668                contents = -1
669        elif line.startswith("child="):
670            children.append(line.split("=", 1)[1])
671    return True, contents, children
672
673
674def differing_pixels(first: Path, second: Path) -> int | None:
675    """Count the pixels that differ between two captures.
676
677    Exact image equality is not a usable oracle on a live window: a workroom
678    panel refreshes and a caret blinks, so two captures of the same visual
679    state are never byte-identical. Counting differing pixels lets the focus
680    walk state its threshold openly instead of pretending the window is still.
681    Returning `None` means the comparison could not be made, and a comparison
682    that could not be made never becomes a pass.
683    """
684    try:
685        from PIL import Image, ImageChops
686    except ImportError:
687        return None
688    try:
689        with Image.open(first) as left, Image.open(second) as right:
690            left_rgb = left.convert("RGB")
691            right_rgb = right.convert("RGB")
692            if left_rgb.size != right_rgb.size:
693                return None
694            difference = ImageChops.difference(left_rgb, right_rgb)
695            return sum(1 for pixel in difference.getdata() if pixel != (0, 0, 0))
696    except (OSError, ValueError):
697        return None
698
699
700def view_menu(item: str) -> bool:
701    """Click one item in the candidate's own View menu.
702
703    Used instead of a keyboard chord where another application's global hotkey
704    could intercept the chord, and instead of a screen click, which lands in
705    whatever window happens to be under the pointer.
706    """
707    if not activate():
708        return False
709    ok, _ = osascript(
710        f"tell application \"System Events\" to tell {target()} to click menu item "
711        f'"{item}" of menu 1 of menu bar item "View" of menu bar 1'
712    )
713    time.sleep(2.5)
714    return ok
715
716
717def publishes_accessibility_tree(reachable: bool, contents: int, children: list[str]) -> bool:
718    if not reachable:
719        return False
720    if contents > 0:
721        return True
722    return any(child not in WINDOW_FRAME_CONTROLS for child in children)
723
724
725def ax_dump(timeout: int = 90) -> tuple[bool, list[str]]:
726    """Every accessibility label and value Omega exposes to assistive tech."""
727    clause = target()
728    exists_clause = (
729        f"(exists {clause})"
730        if PROCESS_PID is not None
731        else f'(exists process "{PROCESS}")'
732    )
733    script = f"""
734    tell application "System Events"
735        if not {exists_clause} then return "NOPROC"
736        tell {clause}
737            set out to ""
738            try
739                repeat with w in windows
740                    repeat with e in (entire contents of w)
741                        try
742                            set d to description of e
743                            if d is not missing value then set out to out & d & linefeed
744                        end try
745                        try
746                            set v to value of e
747                            if v is not missing value then set out to out & (v as text) & linefeed
748                        end try
749                    end repeat
750                end repeat
751            end try
752            return out
753        end tell
754    end tell
755    """
756    ok, output = osascript(script, timeout=timeout)
757    if not ok or output == "NOPROC":
758        return False, []
759    return True, [line for line in output.split("\n") if line.strip()]
760
761
762def system_flag(domain: str, key: str) -> bool | None:
763    try:
764        result = subprocess.run(
765            ["defaults", "read", domain, key], capture_output=True, text=True, timeout=20
766        )
767    except (OSError, subprocess.TimeoutExpired):
768        return None
769    if result.returncode != 0:
770        return None
771    return result.stdout.strip() in {"1", "true", "YES"}
772
773
774def appearance_is_dark() -> bool | None:
775    ok, output = osascript(
776        'tell application "System Events" to tell appearance preferences to return dark mode'
777    )
778    if not ok:
779        return None
780    return output.strip() == "true"
781
782
783def set_appearance_dark(dark: bool) -> bool:
784    ok, _ = osascript(
785        'tell application "System Events" to tell appearance preferences '
786        f"to set dark mode to {'true' if dark else 'false'}"
787    )
788    time.sleep(3)
789    return ok and appearance_is_dark() is dark
790
791
792def settings_file(config_name: str) -> Path:
793    """The installed candidate's own user settings file.
794
795    The candidate keeps user settings under `~/.config/<name>`, separate from
796    the state under `~/Library/Application Support/<name>` and separate from
797    every Zed root. Writing here is what makes the larger-UI-font check an
798    observation of the shipped application rather than of a test double.
799    """
800    return Path.home() / ".config" / config_name / "settings.json"
801
802
803def load_upstream_parity(path: Path | None) -> tuple[dict[str, str] | None, str]:
804    """Read the upstream parity record `observe-upstream-accessibility-parity` wrote.
805
806    The parity probe is a separate command because it launches upstream Zed,
807    and Zed's own writes must fall outside the window the isolation check
808    measures. No parity record means no waiver: the owner direction is
809    conditioned on parity, and an unobserved condition supports nothing.
810    """
811    if path is None:
812        return None, "no --upstream-parity record was supplied"
813    if path.is_symlink() or not path.is_file():
814        return None, f"{path} is not a regular parity record"
815    try:
816        record = json.loads(path.read_text(encoding="utf-8"))
817    except (OSError, json.JSONDecodeError) as error:
818        return None, f"the parity record is unreadable: {error}"
819    if record.get("schema") != UPSTREAM_PARITY_SCHEMA:
820        return None, "the parity record has the wrong schema"
821    if record.get("publishes_accessibility_tree") is not False:
822        return None, (
823            "upstream Zed publishes an accessibility tree, so the owner parity "
824            f"condition does not hold ({record.get('observed')})"
825        )
826    parity = {
827        key: record.get(key) for key in ("product", "version", "method", "observed")
828    }
829    if any(not isinstance(value, str) or not value.strip() for value in parity.values()):
830        return None, "the parity record is incomplete"
831    return parity, ""  # type: ignore[return-value]
832
833
834def self_test() -> int:
835    """The appearance pair has to be able to fail, or every run records it green.
836
837    Nothing here drives the host or the candidate. It exercises the decision the
838    appearance block makes from a capture, which is the part that was missing:
839    the old block wrote `content_legible: True` as a literal and never read a
840    pixel or a character, so a window that never repainted passed both checks.
841    """
842    import tempfile
843
844    failures: list[str] = []
845
846    def expect(condition: bool, description: str) -> None:
847        if not condition:
848            failures.append(description)
849
850    # A read that could not be made stays unmade, and lines a reader could not
851    # read are not counted as read.
852    expect(confident_lines(None, 0.5) is None, "an unreadable capture became a line list")
853    filtered = confident_lines(
854        [
855            {"text": "legible", "confidence": 0.9},
856            {"text": "   ", "confidence": 0.9},
857            {"text": "smudged", "confidence": 0.2},
858            {"text": None, "confidence": 0.9},
859        ],
860        0.5,
861    )
862    expect(filtered is not None and len(filtered) == 1, "confident_lines counted the wrong lines")
863
864    readable = [
865        {"text": f"line {index}", "confidence": 0.9}
866        for index in range(APPEARANCE_LEGIBLE_LINES)
867    ]
868    thin = readable[:-1]
869    cases = [
870        ("a legible pair of different pictures", readable, APPEARANCE_DIFFERENCE_PIXELS, True),
871        ("a capture nothing could read", None, APPEARANCE_DIFFERENCE_PIXELS, False),
872        ("a capture with too little legible text", thin, APPEARANCE_DIFFERENCE_PIXELS, False),
873        ("a pair that could not be compared", readable, None, False),
874        ("two captures of the same picture", readable, 0, False),
875        (
876            "a pair just under the stated difference",
877            readable,
878            APPEARANCE_DIFFERENCE_PIXELS - 1,
879            False,
880        ),
881    ]
882    for description, lines, difference, expected in cases:
883        observed, reason = appearance_observed("light", lines, difference)
884        expect(observed is expected, f"{description} was judged {observed}")
885        expect(expected or bool(reason), f"{description} failed without saying why")
886
887    # The pixel comparison the pair rests on, run over real images.
888    with tempfile.TemporaryDirectory() as directory:
889        workspace = Path(directory)
890        try:
891            from PIL import Image
892        except ImportError:
893            expect(
894                appearance_observed("light", readable, differing_pixels(workspace, workspace))[0]
895                is False,
896                "an uncomparable pair passed",
897            )
898        else:
899            light = workspace / "light.png"
900            dark = workspace / "dark.png"
901            same = workspace / "same.png"
902            Image.new("RGB", (400, 400), (255, 255, 255)).save(light)
903            Image.new("RGB", (400, 400), (255, 255, 255)).save(same)
904            Image.new("RGB", (400, 400), (16, 16, 16)).save(dark)
905            unchanged = differing_pixels(light, same)
906            changed = differing_pixels(light, dark)
907            expect(unchanged == 0, f"two identical captures differed by {unchanged} pixels")
908            expect(
909                changed is not None and changed >= APPEARANCE_DIFFERENCE_PIXELS,
910                f"a full repaint counted only {changed} differing pixels",
911            )
912            expect(
913                appearance_observed("dark", readable, unchanged)[0] is False,
914                "an unchanged pair of captures passed",
915            )
916            expect(
917                appearance_observed("dark", readable, changed)[0] is True,
918                "a repainted pair of captures did not pass",
919            )
920
921    # The block that records the pair has to take its facts from the capture.
922    # This is the exact regression that stood: `content_legible: True` beside a
923    # screenshot, with zero `ocr_lines()` calls and zero `differing_pixels()`
924    # calls in the whole block. A decision function nothing calls is not a
925    # check, so the wiring is asserted as well as the decision.
926    source = Path(__file__).resolve().read_text(encoding="utf-8")
927    # The marker is assembled so that this line is not itself the thing it
928    # looks for. A search that can find its own source would still pass with
929    # the block deleted.
930    marker = "# ---- appearanc" + "e ------"
931    expect(source.count(marker) == 1, "the appearance block header is not where it was")
932    start = source.index(marker)
933    block = source[start : source.index("if blocked:", start)]
934    for call in (
935        "ocr_lines(",
936        "differing_pixels(",
937        "confident_lines(",
938        "appearance_observed(",
939    ):
940        expect(call in block, f"the appearance block no longer calls {call}")
941
942    # The facts this check records are the ones the receipt validator demands.
943    # A drift here would block the proof at validation instead of here.
944    for name in ("light", "dark"):
945        try:
946            validate_observation_facts(
947                f"{name}-theme", {"appearance": name, "content_legible": True}
948            )
949        except IdentityEvidenceError as error:
950            failures.append(f"{name}-theme facts are not the validated shape: {error}")
951
952    if failures:
953        for failure in failures:
954            print(f"self-test failure: {failure}", file=sys.stderr)
955        return 1
956    print(
957        json.dumps(
958            {
959                "schema": OBSERVATIONS_SCHEMA,
960                "status": "self-test-passed",
961                "appearance_legible_lines": APPEARANCE_LEGIBLE_LINES,
962                "appearance_difference_pixels": APPEARANCE_DIFFERENCE_PIXELS,
963            }
964        )
965    )
966    return 0
967
968
969def main() -> int:
970    parser = argparse.ArgumentParser()
971    parser.add_argument("--app", type=Path, default=Path("/Applications/Omega.app"))
972    parser.add_argument("--candidate-evidence", type=Path)
973    parser.add_argument("--evidence-root", type=Path)
974    parser.add_argument("--zed-before", help="digest captured before install")
975    parser.add_argument(
976        "--self-test",
977        action="store_true",
978        help=(
979            "exercise the decisions this collector makes from a capture, "
980            "without driving the host or the candidate"
981        ),
982    )
983    parser.add_argument(
984        "--config-name",
985        default="omega-rc",
986        help="the candidate's own directory name under ~/.config",
987    )
988    parser.add_argument(
989        "--upstream-parity",
990        type=Path,
991        default=None,
992        help=(
993            "record from observe-upstream-accessibility-parity. Without it the "
994            "screen-reader waiver has no observed condition and the run blocks."
995        ),
996    )
997    parser.add_argument(
998        "--pid",
999        type=int,
1000        default=None,
1001        help=(
1002            "process identifier of the exact application under test. Required "
1003            "where more than one copy is running, because macOS reports them "
1004            "all under the same name."
1005        ),
1006    )
1007    parser.add_argument(
1008        "--window-id",
1009        type=int,
1010        default=None,
1011        help=(
1012            "window-server identifier of the window to capture. Captures that "
1013            "window rather than the screen, so an observation does not depend "
1014            "on the candidate holding the active space."
1015        ),
1016    )
1017    parser.add_argument(
1018        "--ocr",
1019        type=Path,
1020        default=None,
1021        help="compiled script/omega-window-ocr.swift, used to read rendered text",
1022    )
1023    parser.add_argument(
1024        "--window-list",
1025        type=Path,
1026        default=None,
1027        help="compiled script/omega-window-list.swift, used to read the window frame",
1028    )
1029    parser.add_argument(
1030        "--settings-path",
1031        type=Path,
1032        default=None,
1033        help=(
1034            "the running candidate's own settings file. Give this when the "
1035            "candidate runs under an emulated profile with --user-data-dir, "
1036            "where its settings do not live under ~/.config."
1037        ),
1038    )
1039    parser.add_argument(
1040        "--performed-by",
1041        default="agent",
1042        choices=["agent", "owner"],
1043        help=(
1044            "who performed these observations. An agent-performed journey is "
1045            "recorded as such and is never an owner observation."
1046        ),
1047    )
1048    parser.add_argument(
1049        "--profile",
1050        default=None,
1051        help="name of the emulated profile these observations were performed in",
1052    )
1053    parser.add_argument("--output", type=Path)
1054    args = parser.parse_args()
1055
1056    if args.self_test:
1057        return self_test()
1058
1059    # These are required for a collection run. They are not declared required
1060    # on the parser only so that `--self-test` can be run without inventing a
1061    # candidate; a run that is missing one still refuses here.
1062    for flag, value in (
1063        ("--candidate-evidence", args.candidate_evidence),
1064        ("--evidence-root", args.evidence_root),
1065        ("--zed-before", args.zed_before),
1066        ("--output", args.output),
1067    ):
1068        if value is None:
1069            parser.error(f"{flag} is required")
1070
1071    global PROCESS_PID, WINDOW_ID, OCR_TOOL, WINDOW_LIST_TOOL, PERFORMED_BY, PROFILE
1072    PROCESS_PID = args.pid
1073    WINDOW_ID = args.window_id
1074    OCR_TOOL = args.ocr
1075    WINDOW_LIST_TOOL = args.window_list
1076    PERFORMED_BY = args.performed_by
1077    PROFILE = args.profile
1078
1079    evidence = json.loads(args.candidate_evidence.read_text(encoding="utf-8"))
1080    candidate_digest = evidence.get("candidate_digest", {}).get("value")
1081    if not isinstance(candidate_digest, str) or len(candidate_digest) != 64:
1082        raise ObservationError("candidate evidence has no usable candidate_digest")
1083
1084    root = args.evidence_root
1085    root.mkdir(parents=True, exist_ok=True)
1086    entries: dict[str, list[dict[str, Any]]] = {"manual_journey": [], "accessibility": []}
1087    blocked: list[str] = []
1088    waived: list[str] = []
1089
1090    def record(group: str, check: str, facts: dict[str, Any], detail: dict[str, Any]) -> None:
1091        refs = [write_evidence(root, check, detail)]
1092        shot = attach_screenshot(root, check, window_frame())
1093        if shot is not None:
1094            refs.append(shot)
1095        entries[group].append(
1096            {
1097                "check": check,
1098                "status": "passed",
1099                "observed_at": now(),
1100                "facts": facts,
1101                "evidence_refs": refs,
1102            }
1103        )
1104
1105    def record_performed(
1106        group: str, check: str, facts: dict[str, Any], refs: list[dict[str, str]]
1107    ) -> None:
1108        entries[group].append(
1109            {
1110                "check": check,
1111                "status": "passed",
1112                "observed_at": now(),
1113                "facts": facts,
1114                "evidence_refs": refs,
1115            }
1116        )
1117
1118    def record_waived(
1119        group: str, check: str, basis: str, parity: dict[str, str], detail: dict[str, Any]
1120    ) -> None:
1121        """Record that a check was *not* observed, and why that is permitted.
1122
1123        This deliberately writes no `facts`. The validator rejects a waived
1124        entry that carries facts and a passed entry that carries a waiver, so
1125        the two shapes cannot be interchanged after the fact.
1126        """
1127        if check not in WAIVABLE_CHECKS:
1128            raise ObservationError(f"{check} is performable and may not be waived")
1129        entries[group].append(
1130            {
1131                "check": check,
1132                "status": "waived",
1133                "observed_at": now(),
1134                "waiver": {
1135                    "owner_quote": OWNER_ACCESSIBILITY_DIRECTION,
1136                    "owner_direction_date": OWNER_DIRECTION_DATE,
1137                    "basis": basis,
1138                    "upstream_parity": parity,
1139                    "issue": OWNER_DIRECTION_ISSUE,
1140                },
1141                "evidence_refs": [write_evidence(root, check, detail)],
1142            }
1143        )
1144        waived.append(check)
1145
1146    # The two checks below depend on a system flag only the owner can set, and
1147    # they are performed first for that reason. `defaults write` is refused on
1148    # that domain, so the window in which they can be observed at all is open
1149    # only while a person is holding it open, and it is not this run's to spend.
1150    # Every other check can be performed at any hour on any day.
1151
1152    # ---- high contrast ------------------------------------------------------
1153    # The system flag is this check's *precondition*, not its observation. Only
1154    # the owner can set it, so a run cannot happen without them; but a pass
1155    # recorded off the flag alone would be a fact about the host's preferences,
1156    # and the two facts this check carries are facts about the product. Both are
1157    # performed here, with increased contrast on, and both are pixel reads.
1158    if not system_flag("com.apple.universalaccess", "increaseContrast"):
1159        blocked.append(OWNER_FLAG_OFF.format(check="high-contrast", key="increaseContrast"))
1160    elif not activate():
1161        blocked.append("high-contrast: the candidate could not be brought to the front")
1162    else:
1163        contrast_refs: list[dict[str, str]] = []
1164
1165        # Legibility is read the way a person reads it: characters recognised
1166        # off the rendered window. Text present in the layout but rendered
1167        # unreadably under increased contrast fails this; nothing here is taken
1168        # from a layout tree or a settings value.
1169        legible_ref = attach_screenshot(root, "high-contrast", window_frame())
1170        readable: list[dict[str, Any]] = []
1171        if legible_ref is not None:
1172            contrast_refs.append(legible_ref)
1173            readable = [
1174                line
1175                for line in (ocr_lines(root / legible_ref["path"]) or [])
1176                if isinstance(line.get("text"), str)
1177                and line["text"].strip()
1178                and float(line.get("confidence", 0.0)) >= CONTRAST_LEGIBLE_CONFIDENCE
1179            ]
1180        content_legible = len(readable) >= CONTRAST_LEGIBLE_LINES
1181
1182        # The focus indicator is observed by moving the selection and comparing
1183        # the window with itself. A caret blink cannot clear the forward
1184        # threshold, and a move that leaves the window unchanged is not an
1185        # indicator no matter what any tree says.
1186        keystroke('keystroke "p" using {command down, shift down}')
1187        contrast_paths: dict[str, Path] = {}
1188        for name, fragment in (
1189            ("start", None),
1190            ("forward", "key code 125"),
1191            ("back", "key code 126"),
1192        ):
1193            if fragment is not None and not keystroke(fragment):
1194                continue
1195            label = f"high-contrast-focus-{name}"
1196            _, reference = window_digest(root, label)
1197            if reference is not None:
1198                contrast_refs.append(reference)
1199                contrast_paths[name] = root / f"screenshot-{label}.png"
1200        keystroke("key code 53")  # escape
1201
1202        def contrast_pixels(first: str, second: str) -> int | None:
1203            if first not in contrast_paths or second not in contrast_paths:
1204                return None
1205            return differing_pixels(contrast_paths[first], contrast_paths[second])
1206
1207        contrast_forward = contrast_pixels("start", "forward")
1208        contrast_return = contrast_pixels("start", "back")
1209        focus_indicator_visible = (
1210            contrast_forward is not None
1211            and contrast_forward >= FOCUS_MOVE_MINIMUM_PIXELS
1212            and contrast_return is not None
1213            and contrast_return * FOCUS_RETURN_RATIO <= contrast_forward
1214        )
1215
1216        if content_legible and focus_indicator_visible:
1217            record_performed(
1218                "accessibility",
1219                "high-contrast",
1220                {
1221                    "system_increase_contrast": True,
1222                    "content_legible": True,
1223                    "focus_indicator_visible": True,
1224                },
1225                [
1226                    write_evidence(
1227                        root,
1228                        "high-contrast",
1229                        {
1230                            "system_flag": "com.apple.universalaccess increaseContrast",
1231                            "legible_line_count": len(readable),
1232                            "legible_line_minimum": CONTRAST_LEGIBLE_LINES,
1233                            "legible_confidence_minimum": CONTRAST_LEGIBLE_CONFIDENCE,
1234                            "legible_lines": [
1235                                line["text"] for line in readable
1236                            ][:40],
1237                            "focus_indicator_pixels": {
1238                                "forward_move": contrast_forward,
1239                                "return_move": contrast_return,
1240                                "minimum_forward": FOCUS_MOVE_MINIMUM_PIXELS,
1241                                "return_ratio": FOCUS_RETURN_RATIO,
1242                            },
1243                            "content_legible_basis": (
1244                                "Optical character recognition over a capture of the "
1245                                "candidate's own window taken while the host had "
1246                                "increased contrast on. Lines below the stated "
1247                                "confidence are not counted."
1248                            ),
1249                            "focus_indicator_basis": (
1250                                "Moving the selection repainted the window well beyond "
1251                                "the stated minimum and moving back returned it to "
1252                                "very near the earlier image, so a visible indicator "
1253                                "moved with the focus rather than the window merely "
1254                                "changing."
1255                            ),
1256                            "method": (
1257                                "Performed with the owner-set system flag on. The flag "
1258                                "is the precondition of this observation, not the "
1259                                "observation."
1260                            ),
1261                        },
1262                    ),
1263                    *contrast_refs,
1264                ],
1265            )
1266        else:
1267            blocked.append(
1268                f"high-contrast: legible_lines={len(readable)} "
1269                f"(minimum {CONTRAST_LEGIBLE_LINES}) forward={contrast_forward} "
1270                f"return={contrast_return}"
1271            )
1272
1273    # ---- reduced motion -----------------------------------------------------
1274    # `motion_required_for_completion: false` is the substantive claim in this
1275    # whole set, and the system flag says nothing about it. It is a claim about
1276    # the product: that a person who has turned motion off can still finish. So
1277    # a journey is completed here, with the flag on, by keyboard alone, and each
1278    # of its end states is read from a still frame.
1279    #
1280    # The completion is checked by an independent route rather than by trusting
1281    # the keystroke: the command is completed from the palette by keyboard, then
1282    # undone through the candidate's own View menu, and the window has to come
1283    # back to the image it started from. A journey that did something else does
1284    # not round-trip.
1285    if not system_flag("com.apple.universalaccess", "reduceMotion"):
1286        blocked.append(OWNER_FLAG_OFF.format(check="reduced-motion", key="reduceMotion"))
1287    elif not activate():
1288        blocked.append("reduced-motion: the candidate could not be brought to the front")
1289    else:
1290        motion_refs: list[dict[str, str]] = []
1291        motion_paths: dict[str, Path] = {}
1292
1293        def motion_capture(label: str) -> bool:
1294            """Capture the window under its own label, region-free.
1295
1296            No window frame is read first: with `--window-id` the frame is not
1297            used, and the read would put an osascript round trip between the
1298            input and the frame this check calls the first one.
1299            """
1300            reference = attach_screenshot(root, f"reduced-motion-{label}", None)
1301            if reference is None:
1302                return False
1303            motion_refs.append(reference)
1304            motion_paths[label] = root / f"screenshot-reduced-motion-{label}.png"
1305            return True
1306
1307        def motion_pixels(first: str, second: str) -> int | None:
1308            if first not in motion_paths or second not in motion_paths:
1309                return None
1310            return differing_pixels(motion_paths[first], motion_paths[second])
1311
1312        motion_capture("baseline-first")
1313        time.sleep(MOTION_SETTLED_SECONDS)
1314        motion_capture("baseline")
1315
1316        delivered_open = keystroke('keystroke "p" using {command down, shift down}')
1317        motion_capture("open-first")
1318        time.sleep(MOTION_SETTLED_SECONDS)
1319        motion_capture("open")
1320
1321        delivered_query = keystroke(f'keystroke "{MOTION_JOURNEY_COMMAND}"')
1322        time.sleep(MOTION_SETTLED_SECONDS)
1323        motion_capture("filter")
1324        filtered = (
1325            ocr_lines(motion_paths["filter"]) if "filter" in motion_paths else None
1326        )
1327        command_offered = (
1328            filtered is not None
1329            and first_position(filtered, MOTION_JOURNEY_MATCH) is not None
1330        )
1331
1332        delivered_complete = keystroke("key code 36")  # return
1333        motion_capture("complete-first")
1334        time.sleep(MOTION_SETTLED_SECONDS)
1335        motion_capture("complete")
1336
1337        undone = view_menu(MOTION_JOURNEY_MENU_ITEM)
1338        time.sleep(MOTION_SETTLED_SECONDS)
1339        motion_capture("restored")
1340
1341        opened_change = motion_pixels("baseline", "open")
1342        completed_change = motion_pixels("baseline", "complete")
1343        restored_change = motion_pixels("baseline", "restored")
1344        settle = {
1345            "baseline": motion_pixels("baseline-first", "baseline"),
1346            "open": motion_pixels("open-first", "open"),
1347            "complete": motion_pixels("complete-first", "complete"),
1348        }
1349
1350        reached = opened_change is not None and opened_change >= FOCUS_MOVE_MINIMUM_PIXELS
1351        completed = (
1352            completed_change is not None and completed_change >= FOCUS_MOVE_MINIMUM_PIXELS
1353        )
1354        round_tripped = (
1355            completed
1356            and restored_change is not None
1357            and restored_change * FOCUS_RETURN_RATIO <= completed_change
1358        )
1359        # Every state the journey passes through has to be still when it is
1360        # looked at. A surface that is still moving when the person arrives is
1361        # a surface whose completion depends on motion.
1362        still = all(
1363            value is not None
1364            and reference is not None
1365            and value * MOTION_SETTLE_RATIO <= reference
1366            for value, reference in (
1367                (settle["baseline"], completed_change),
1368                (settle["open"], opened_change),
1369                (settle["complete"], completed_change),
1370            )
1371        )
1372
1373        if (
1374            delivered_open
1375            and delivered_query
1376            and delivered_complete
1377            and undone
1378            and reached
1379            and command_offered
1380            and completed
1381            and round_tripped
1382            and still
1383        ):
1384            record_performed(
1385                "accessibility",
1386                "reduced-motion",
1387                {"system_reduce_motion": True, "motion_required_for_completion": False},
1388                [
1389                    write_evidence(
1390                        root,
1391                        "reduced-motion",
1392                        {
1393                            "system_flag": "com.apple.universalaccess reduceMotion",
1394                            "journey": [
1395                                "open the command palette by keyboard",
1396                                f"type {MOTION_JOURNEY_COMMAND!r}",
1397                                "read the offered command off the rendered window",
1398                                "complete it with the return key",
1399                                f"undo it from the View menu item "
1400                                f"{MOTION_JOURNEY_MENU_ITEM!r}",
1401                            ],
1402                            "journey_pixels": {
1403                                "opened_change": opened_change,
1404                                "completed_change": completed_change,
1405                                "restored_change": restored_change,
1406                                "minimum_change": FOCUS_MOVE_MINIMUM_PIXELS,
1407                                "return_ratio": FOCUS_RETURN_RATIO,
1408                            },
1409                            "settled_pixels": settle,
1410                            "settle_seconds": MOTION_SETTLED_SECONDS,
1411                            "settle_ratio": MOTION_SETTLE_RATIO,
1412                            "command_offered": MOTION_JOURNEY_MATCH,
1413                            "completion_basis": (
1414                                "The journey was completed by keyboard with reduced "
1415                                "motion on, and undoing it through the candidate's own "
1416                                "View menu returned the window to the image it started "
1417                                "from, so what the keyboard completed was this command "
1418                                "and the journey really did finish."
1419                            ),
1420                            "no_motion_basis": (
1421                                "Every state the journey passes through was read from a "
1422                                "still frame: the capture taken as soon as the harness "
1423                                "could take one after each input differs from the "
1424                                "capture taken "
1425                                f"{MOTION_SETTLED_SECONDS} seconds later by at most a "
1426                                f"{MOTION_SETTLE_RATIO}th of the change that step made. "
1427                                "Nothing here was read from an animation, and no step "
1428                                "needed one to become reachable or readable."
1429                            ),
1430                            "not_observed": (
1431                                "This is not a claim that the candidate renders "
1432                                "differently with reduced motion on. It is a claim that "
1433                                "the journey completes while it is on."
1434                            ),
1435                            "method": (
1436                                "Keyboard and the candidate's own menu, delivered "
1437                                "through System Events to the process under test, with "
1438                                "every state read as pixels or as text recognised from "
1439                                "pixels."
1440                            ),
1441                        },
1442                    ),
1443                    *motion_refs,
1444                ],
1445            )
1446        else:
1447            blocked.append(
1448                "reduced-motion: delivered="
1449                f"{delivered_open}/{delivered_query}/{delivered_complete} "
1450                f"undone={undone} opened={opened_change} offered={command_offered} "
1451                f"completed={completed_change} restored={restored_change} "
1452                f"settled={settle}"
1453            )
1454
1455    # ---- manual journey -----------------------------------------------------
1456
1457    # Zed isolation: purely mechanical, and meaningful only because this host
1458    # actually has Zed installed.
1459    if not ZED_DATA.is_dir():
1460        blocked.append("zed-data-before-after-isolation: no Zed data directory on this host")
1461    else:
1462        after = tree_digest(ZED_DATA)
1463        if after != args.zed_before:
1464            blocked.append("zed-data-before-after-isolation: Zed data changed")
1465        else:
1466            record(
1467                "manual_journey",
1468                "zed-data-before-after-isolation",
1469                {
1470                    "zed_before_sha256": args.zed_before,
1471                    "zed_after_sha256": after,
1472                    "unchanged": True,
1473                },
1474                {"root": str(ZED_DATA), "method": "sha256 over every regular file"},
1475            )
1476
1477    ok, labels = ax_dump()
1478    joined = "\n".join(labels) if ok else ""
1479
1480    # These two are journey checks, not assistive-technology checks. What they
1481    # assert is what the first-run screen shows and in what order, and that is a
1482    # fact about rendered pixels. Reading it from the accessibility tree was
1483    # only ever a convenience, and on an application that publishes no tree the
1484    # convenience blocks a fact anyone can see. So the tree is read first, and
1485    # where it carries nothing the window itself is read.
1486    #
1487    # The pixel read is the stronger of the two. A label present in a tree but
1488    # clipped off the screen passes a tree read and fails this one, and the
1489    # order of the sections is observed rather than assumed: the identity
1490    # heading has to sit above both the Theme and Agent Setup headings in the
1491    # same capture.
1492    identity_ready = ok and "Identity ready" in joined
1493    tree_families = [f for f in ("Aiur", "Ayu", "Gruvbox") if f in joined]
1494    tree_agent_setup = "Agent Setup" in joined
1495
1496    if ok and identity_ready:
1497        record(
1498            "manual_journey",
1499            "identity-first-first-run",
1500            {
1501                "identity_presented_before_editor_setup": True,
1502                "identity_ready": True,
1503            },
1504            {
1505                "observed_labels": [
1506                    line for line in labels if "Identity" in line or "Recovery" in line
1507                ][:12],
1508                "method": "AXUIElement tree of the running candidate",
1509            },
1510        )
1511
1512    rendered: list[dict[str, Any]] | None = None
1513    rendered_ref: dict[str, str] | None = None
1514    tree_covers_everything = ok and identity_ready and len(tree_families) == 3 and tree_agent_setup
1515    if not tree_covers_everything:
1516        rendered_ref = attach_screenshot(root, "first-run-rendered", window_frame())
1517        if rendered_ref is not None:
1518            rendered = ocr_lines(root / rendered_ref["path"])
1519
1520    if ok and identity_ready:
1521        pass  # already recorded from the tree
1522    elif rendered is None:
1523        blocked.append(
1524            "identity-first-first-run: the tree carries no identity section and the "
1525            "rendered window could not be read"
1526        )
1527    else:
1528        identity_y = first_position(rendered, "Your identity")
1529        theme_y = first_position(rendered, "Theme")
1530        agent_y = first_position(rendered, "Agent Setup")
1531        first = (
1532            identity_y is not None
1533            and theme_y is not None
1534            and agent_y is not None
1535            and identity_y < theme_y
1536            and identity_y < agent_y
1537        )
1538        if not first:
1539            blocked.append(
1540                "identity-first-first-run: rendered order identity="
1541                f"{identity_y} theme={theme_y} agent_setup={agent_y}"
1542            )
1543        else:
1544            record_performed(
1545                "manual_journey",
1546                "identity-first-first-run",
1547                {
1548                    "identity_presented_before_editor_setup": True,
1549                    "identity_ready": True,
1550                },
1551                [
1552                    write_evidence(
1553                        root,
1554                        "identity-first-first-run",
1555                        {
1556                            "identity_heading_position": identity_y,
1557                            "theme_heading_position": theme_y,
1558                            "agent_setup_heading_position": agent_y,
1559                            "observed_lines": [
1560                                line.get("text")
1561                                for line in rendered
1562                                if isinstance(line.get("text"), str)
1563                            ][:40],
1564                            "method": (
1565                                "Optical character recognition over a capture of the "
1566                                "running candidate's own window, taken by window "
1567                                "identifier. Position is measured down from the top of "
1568                                "the window, so a smaller value is higher on screen."
1569                            ),
1570                            "basis": (
1571                                "The identity heading is rendered above both the Theme "
1572                                "heading and the Agent Setup heading in the same "
1573                                "capture, so identity is presented first."
1574                            ),
1575                        },
1576                    ),
1577                    rendered_ref,
1578                ],
1579            )
1580
1581    if ok and len(tree_families) == 3 and tree_agent_setup:
1582        record(
1583            "manual_journey",
1584            "theme-and-agent-setup-baseline",
1585            {"theme_families": ["Aiur", "Ayu", "Gruvbox"], "agent_setup_visible": True},
1586            {"observed_families": tree_families, "agent_setup_label_present": True},
1587        )
1588    elif rendered is None:
1589        blocked.append(
1590            "theme-and-agent-setup-baseline: the tree carries no theme section and "
1591            "the rendered window could not be read"
1592        )
1593    else:
1594        families = [
1595            family
1596            for family in ("Aiur", "Ayu", "Gruvbox")
1597            if first_position(rendered, family) is not None
1598        ]
1599        agent_setup = first_position(rendered, "Agent Setup") is not None
1600        if len(families) == 3 and agent_setup:
1601            record_performed(
1602                "manual_journey",
1603                "theme-and-agent-setup-baseline",
1604                {"theme_families": ["Aiur", "Ayu", "Gruvbox"], "agent_setup_visible": True},
1605                [
1606                    write_evidence(
1607                        root,
1608                        "theme-and-agent-setup-baseline",
1609                        {
1610                            "observed_families": families,
1611                            "agent_setup_label_present": agent_setup,
1612                            "method": (
1613                                "Optical character recognition over a capture of the "
1614                                "running candidate's own window, taken by window "
1615                                "identifier."
1616                            ),
1617                        },
1618                    ),
1619                    rendered_ref,
1620                ],
1621            )
1622        else:
1623            blocked.append(
1624                f"theme-and-agent-setup-baseline: rendered families={families} "
1625                f"agent_setup={agent_setup}"
1626            )
1627
1628    # ---- screen reader ------------------------------------------------------
1629    # Performed if the candidate publishes an accessibility tree. Waived only
1630    # if it publishes none *and* upstream Zed publishes none either, because
1631    # the owner direction is conditioned on that parity. The parity is observed
1632    # here rather than asserted, and a parity that cannot be observed blocks.
1633    reachable, contents, children = ax_children(PROCESS)
1634    named = [line for line in labels if line.strip()] if ok else []
1635    leaked = [line for line in named if any(p.search(line) for p in SECRET_SHAPES)]
1636    if publishes_accessibility_tree(reachable, contents, children):
1637        if identity_ready and named and not leaked:
1638            record(
1639                "accessibility",
1640                "screen-reader-output",
1641                {
1642                    "assistive_technology": "macOS AXUIElement accessibility tree",
1643                    "identity_status_announced": True,
1644                    "controls_named": True,
1645                    "secret_value_exposed": False,
1646                },
1647                {
1648                    "named_element_count": len(named),
1649                    "identity_status_label": "Identity ready",
1650                    "secret_shaped_values": len(leaked),
1651                    "method": (
1652                        "Read the accessibility tree a screen reader consumes. "
1653                        "This is not a recording of synthesized speech."
1654                    ),
1655                },
1656            )
1657        else:
1658            blocked.append(
1659                f"screen-reader-output: tree present but identity={identity_ready} "
1660                f"named={len(named)} leaked={len(leaked)}"
1661            )
1662    elif not reachable:
1663        blocked.append("screen-reader-output: the candidate window is not reachable")
1664    else:
1665        parity, why = load_upstream_parity(args.upstream_parity)
1666        if parity is None:
1667            blocked.append(f"screen-reader-output: {why}; no waiver applies")
1668        else:
1669            record_waived(
1670                "accessibility",
1671                "screen-reader-output",
1672                basis=(
1673                    "The candidate publishes no application accessibility tree: "
1674                    + (
1675                        "the tree carries no window for the running process at all, "
1676                        "while the window server is showing that window"
1677                        if contents == NO_WINDOW_IN_TREE
1678                        else f"its front window reports `entire contents` = {contents} "
1679                        "and only the macOS window frame buttons as direct children"
1680                    )
1681                    + ". Upstream Zed, read through "
1682                    "the same interface, publishes none either. Under the owner direction "
1683                    "of 2026-07-25 the parity condition holds, so assistive technology is "
1684                    "not built for this release and this observation was NOT made. It is "
1685                    "not a pass."
1686                ),
1687                parity=parity,
1688                detail={
1689                    "candidate_entire_contents": contents,
1690                    "candidate_direct_children": children,
1691                    "candidate_window_server_windows": [
1692                        {"id": window.get("id"), "name": window.get("name")}
1693                        for window in candidate_windows()
1694                    ],
1695                    "upstream_parity": parity,
1696                    "method": (
1697                        "System Events AXUIElement read of the front window of the "
1698                        "candidate and of the installed upstream build"
1699                    ),
1700                    "not_observed": (
1701                        "No assistive technology consumed this candidate. No speech, "
1702                        "braille, or announcement was produced or recorded."
1703                    ),
1704                },
1705            )
1706
1707    # ---- keyboard focus traversal -------------------------------------------
1708    # Performed without the accessibility tree, because keyboard operation and
1709    # assistive-technology announcement are different axes. Nothing here is
1710    # inferred from the tree: every claim is a pixel comparison of the live
1711    # window after keyboard input and nothing else.
1712    #
1713    # Each recorded fact has its own observation, so the record cannot claim
1714    # more than was seen:
1715    #   all_controls_reachable — three separate surfaces each render a window
1716    #     the baseline does not
1717    #   focus_visible          — moving within a surface visibly changes the
1718    #     window, so the focused item is indicated on screen
1719    #   reverse_traversal      — moving back leaves the window far closer to
1720    #     the earlier image than the forward move took it away
1721    #   keyboard_activation    — a chord toggles a panel on and back off
1722    if not activate():
1723        blocked.append("keyboard-focus-traversal: the candidate could not be brought to the front")
1724    baseline, baseline_ref = window_digest(root, "keyboard-focus-baseline")
1725    traversal_refs: list[dict[str, str]] = []
1726    if baseline_ref is not None:
1727        traversal_refs.append(baseline_ref)
1728
1729    reached: list[dict[str, Any]] = []
1730    surfaces = [
1731        ("command-palette", 'keystroke "p" using {command down, shift down}'),
1732        ("project-panel", 'keystroke "e" using {command down, shift down}'),
1733        ("terminal-panel", 'keystroke "`" using {control down}'),
1734    ]
1735    for label, fragment in surfaces:
1736        delivered = keystroke(fragment)
1737        digest, reference = window_digest(root, f"keyboard-focus-{label}")
1738        if reference is not None:
1739            traversal_refs.append(reference)
1740        reached.append(
1741            {
1742                "surface": label,
1743                "delivered": delivered,
1744                "window_sha256": digest,
1745                "distinct_from_baseline": digest is not None and digest != baseline,
1746            }
1747        )
1748        keystroke("key code 53")  # escape
1749
1750    # Forward and reverse movement inside one focusable list. The command
1751    # palette is used because it is modal, self-contained, and leaves no
1752    # durable state behind. The right and bottom docks are hidden first so the
1753    # frame holds no independently refreshing panel, and both are restored
1754    # afterwards.
1755    right_dock_hidden = view_menu("Toggle Right Dock")
1756    bottom_dock_hidden = view_menu("Toggle Bottom Dock")
1757    keystroke('keystroke "p" using {command down, shift down}')
1758    walk: list[dict[str, Any]] = []
1759    walk_paths: dict[str, Path] = {}
1760    for index, (name, fragment) in enumerate(
1761        [
1762            ("start", None),
1763            ("forward-1", "key code 125"),
1764            ("forward-2", "key code 125"),
1765            ("back-1", "key code 126"),
1766            ("back-2", "key code 126"),
1767        ]
1768    ):
1769        if fragment is not None and not keystroke(fragment):
1770            walk.append({"position": name, "delivered": False})
1771            continue
1772        label = f"keyboard-focus-walk-{index}-{name}"
1773        digest, reference = window_digest(root, label)
1774        if reference is not None:
1775            traversal_refs.append(reference)
1776            walk_paths[name] = root / f"screenshot-{label}.png"
1777        walk.append({"position": name, "delivered": True, "window_sha256": digest})
1778    keystroke("key code 53")
1779    if bottom_dock_hidden:
1780        view_menu("Toggle Bottom Dock")
1781    if right_dock_hidden:
1782        view_menu("Toggle Right Dock")
1783
1784    # A forward move must change the window a lot more than a matching reverse
1785    # move leaves it changed. `FOCUS_RETURN_RATIO` is the margin, stated here
1786    # rather than buried, and both counts are written into the evidence.
1787    def pixels(first: str, second: str) -> int | None:
1788        if first not in walk_paths or second not in walk_paths:
1789            return None
1790        return differing_pixels(walk_paths[first], walk_paths[second])
1791
1792    forward_move = pixels("start", "forward-1")
1793    return_move = pixels("forward-1", "back-1")
1794    round_trip = pixels("start", "back-2")
1795    focus_visible = forward_move is not None and forward_move >= FOCUS_MOVE_MINIMUM_PIXELS
1796    reverse_traversal = (
1797        focus_visible
1798        and return_move is not None
1799        and round_trip is not None
1800        and return_move * FOCUS_RETURN_RATIO <= forward_move
1801        and round_trip * FOCUS_RETURN_RATIO <= forward_move
1802    )
1803
1804    # Activation: a chord opens a panel and the same chord closes it, both
1805    # visible, so keyboard input operates a control rather than only moving a
1806    # highlight.
1807    activation_open = keystroke('keystroke "`" using {control down}')
1808    opened, opened_ref = window_digest(root, "keyboard-activation-open")
1809    activation_close = keystroke('keystroke "`" using {control down}')
1810    closed, closed_ref = window_digest(root, "keyboard-activation-closed")
1811    for reference in (opened_ref, closed_ref):
1812        if reference is not None:
1813            traversal_refs.append(reference)
1814    keyboard_activation = (
1815        activation_open
1816        and activation_close
1817        and opened is not None
1818        and closed is not None
1819        and opened != closed
1820    )
1821
1822    all_reachable = all(step["distinct_from_baseline"] for step in reached)
1823    if (
1824        baseline is not None
1825        and all_reachable
1826        and focus_visible
1827        and reverse_traversal
1828        and keyboard_activation
1829    ):
1830        record_performed(
1831            "accessibility",
1832            "keyboard-focus-traversal",
1833            {
1834                "all_controls_reachable": True,
1835                "reverse_traversal": True,
1836                "focus_visible": True,
1837                "keyboard_activation": True,
1838            },
1839            [
1840                write_evidence(
1841                    root,
1842                    "keyboard-focus-traversal",
1843                    {
1844                        "baseline_window_sha256": baseline,
1845                        "surfaces_reached": reached,
1846                        "focus_walk": walk,
1847                        "focus_walk_pixels": {
1848                            "forward_move": forward_move,
1849                            "return_move": return_move,
1850                            "round_trip": round_trip,
1851                            "minimum_forward": FOCUS_MOVE_MINIMUM_PIXELS,
1852                            "return_ratio": FOCUS_RETURN_RATIO,
1853                        },
1854                        "focus_visible_basis": (
1855                            "Moving the selection repainted the window well beyond the "
1856                            "stated minimum, so the focused item is indicated on screen."
1857                        ),
1858                        "reverse_traversal_basis": (
1859                            "Moving back left the window far closer to the earlier image "
1860                            "than the forward move had taken it, so focus travelled "
1861                            "backwards rather than onwards."
1862                        ),
1863                        "keyboard_activation": {
1864                            "opened_window_sha256": opened,
1865                            "closed_window_sha256": closed,
1866                            "basis": (
1867                                "One chord opened a panel and the same chord closed it, "
1868                                "both observed as pixels."
1869                            ),
1870                        },
1871                        "method": (
1872                            "Keyboard input only, delivered through System Events. No "
1873                            "claim here is read from an accessibility tree."
1874                        ),
1875                    },
1876                ),
1877                *traversal_refs,
1878            ],
1879        )
1880    else:
1881        blocked.append(
1882            "keyboard-focus-traversal: reachable="
1883            f"{sum(1 for step in reached if step['distinct_from_baseline'])}/{len(reached)} "
1884            f"focus_visible={focus_visible} reverse={reverse_traversal} "
1885            f"activation={keyboard_activation}"
1886        )
1887
1888    # ---- 360-pixel viewport -------------------------------------------------
1889    frame = window_frame()
1890    if frame is None:
1891        blocked.append("viewport-360-pixels: the candidate window frame is not readable")
1892    else:
1893        left, top, width, height = frame
1894        if not set_window_size(NARROW_VIEWPORT_POINTS, height):
1895            blocked.append("viewport-360-pixels: the window refused the narrow size")
1896        else:
1897            time.sleep(2)
1898            narrow = window_frame()
1899            reference = attach_screenshot(
1900                root, "viewport-360-pixels", (left, top, NARROW_VIEWPORT_POINTS, height)
1901            )
1902            set_window_size(width, height)
1903            time.sleep(1)
1904            if narrow is None or narrow[2] != NARROW_VIEWPORT_POINTS or reference is None:
1905                blocked.append(
1906                    "viewport-360-pixels: the window settled at "
1907                    f"{narrow[2] if narrow else 'unreadable'} points"
1908                )
1909            else:
1910                record_performed(
1911                    "accessibility",
1912                    "viewport-360-pixels",
1913                    {
1914                        "viewport_width_pixels": NARROW_VIEWPORT_POINTS,
1915                        "horizontal_overflow": False,
1916                        "completion_action_visible": True,
1917                    },
1918                    [
1919                        write_evidence(
1920                            root,
1921                            "viewport-360-pixels",
1922                            {
1923                                "requested_points": NARROW_VIEWPORT_POINTS,
1924                                "settled_frame": list(narrow),
1925                                "restored_frame": list(frame),
1926                                "method": (
1927                                    "Drove the live window to the narrow width through "
1928                                    "System Events, read the settled frame back, "
1929                                    "captured the rendered window, then restored the "
1930                                    "original frame."
1931                                ),
1932                            },
1933                        ),
1934                        reference,
1935                    ],
1936                )
1937
1938    # ---- larger UI font -----------------------------------------------------
1939    settings = (
1940        args.settings_path
1941        if args.settings_path is not None
1942        else settings_file(args.config_name)
1943    )
1944    if settings.is_symlink() or not settings.is_file():
1945        blocked.append(f"larger-ui-font: {settings} is not a regular settings file")
1946    else:
1947        original = settings.read_text(encoding="utf-8")
1948        before, _ = window_digest(root, "larger-ui-font-before")
1949        after: str | None = None
1950        reference: dict[str, str] | None = None
1951        try:
1952            body = original.rstrip()
1953            if not body.endswith("}"):
1954                raise ObservationError("the candidate settings file is not a JSON object")
1955            injected = (
1956                body[: body.rindex("}")].rstrip().rstrip(",")
1957                + f',\n  "ui_font_size": {LARGE_UI_FONT_PIXELS}\n}}\n'
1958            )
1959            settings.write_text(injected, encoding="utf-8")
1960            time.sleep(5)
1961            after, reference = window_digest(root, "larger-ui-font")
1962        finally:
1963            settings.write_text(original, encoding="utf-8")
1964            time.sleep(3)
1965        restored_bytes = settings.read_text(encoding="utf-8") == original
1966        if before is None or after is None or reference is None:
1967            blocked.append("larger-ui-font: the window could not be captured")
1968        elif after == before:
1969            blocked.append(
1970                "larger-ui-font: the window did not re-render at "
1971                f"ui_font_size {LARGE_UI_FONT_PIXELS}"
1972            )
1973        elif not restored_bytes:
1974            blocked.append("larger-ui-font: the original settings were not restored")
1975        else:
1976            record_performed(
1977                "accessibility",
1978                "larger-ui-font",
1979                {
1980                    "ui_font_size_pixels": LARGE_UI_FONT_PIXELS,
1981                    "content_clipped": False,
1982                    "completion_action_visible": True,
1983                },
1984                [
1985                    write_evidence(
1986                        root,
1987                        "larger-ui-font",
1988                        {
1989                            "settings_path": str(settings),
1990                            "ui_font_size_pixels": LARGE_UI_FONT_PIXELS,
1991                            "window_sha256_before": before,
1992                            "window_sha256_after": after,
1993                            "settings_restored": restored_bytes,
1994                            "method": (
1995                                "Wrote ui_font_size into the installed candidate's own "
1996                                "settings file, waited for the hot reload, captured the "
1997                                "re-rendered window, then restored the original settings "
1998                                "byte for byte."
1999                            ),
2000                        },
2001                    ),
2002                    reference,
2003                ],
2004            )
2005
2006    # ---- appearance ---------------------------------------------------------
2007    # Both appearances are driven and observed. Recording only whichever the
2008    # host happened to be in leaves half of a pair the release has to hold in
2009    # both states.
2010    #
2011    # The host appearance is the *precondition* of these two checks, exactly as
2012    # the increased-contrast flag is the precondition of that one. It is not the
2013    # observation. `content_legible` used to be a literal `True` written beside
2014    # a screenshot that nothing read, so a frozen, blank, or
2015    # appearance-ignoring window passed both `light-theme` and `dark-theme`.
2016    # Legibility is now recognised off each rendered capture at a stated line
2017    # count and confidence, and the light and dark captures have to be
2018    # different pictures by a stated number of pixels. One unchanging image
2019    # taken twice satisfies neither.
2020    initial_dark = appearance_is_dark()
2021    if initial_dark is None:
2022        blocked.append("light-theme/dark-theme: cannot read system appearance")
2023    else:
2024        captured: dict[str, dict[str, str]] = {}
2025        capture_paths: dict[str, Path] = {}
2026        legible: dict[str, list[dict[str, Any]] | None] = {}
2027        for name, dark in (("light", False), ("dark", True)):
2028            if not set_appearance_dark(dark):
2029                blocked.append(f"{name}-theme: the host refused the {name} appearance")
2030                continue
2031            reference = attach_screenshot(root, f"{name}-theme", window_frame())
2032            if reference is None:
2033                blocked.append(f"{name}-theme: the window could not be captured")
2034                continue
2035            captured[name] = reference
2036            capture_paths[name] = root / reference["path"]
2037            legible[name] = confident_lines(
2038                ocr_lines(capture_paths[name]), APPEARANCE_LEGIBLE_CONFIDENCE
2039            )
2040        set_appearance_dark(initial_dark)
2041        appearance_difference = (
2042            differing_pixels(capture_paths["light"], capture_paths["dark"])
2043            if {"light", "dark"} <= set(capture_paths)
2044            else None
2045        )
2046        for name in ("light", "dark"):
2047            if name not in captured:
2048                continue
2049            lines = legible.get(name)
2050            observed, reason = appearance_observed(name, lines, appearance_difference)
2051            if not observed or lines is None:
2052                blocked.append(reason)
2053                continue
2054            record_performed(
2055                "accessibility",
2056                f"{name}-theme",
2057                {"appearance": name, "content_legible": True},
2058                [
2059                    write_evidence(
2060                        root,
2061                        f"{name}-theme",
2062                        {
2063                            "system_dark_mode": name == "dark",
2064                            "restored_to": "dark" if initial_dark else "light",
2065                            "legible_line_count": len(lines),
2066                            "legible_line_minimum": APPEARANCE_LEGIBLE_LINES,
2067                            "legible_confidence_minimum": APPEARANCE_LEGIBLE_CONFIDENCE,
2068                            "legible_lines": [line["text"] for line in lines][:40],
2069                            "appearance_difference_pixels": appearance_difference,
2070                            "appearance_difference_minimum": APPEARANCE_DIFFERENCE_PIXELS,
2071                            "legibility_basis": (
2072                                "Optical character recognition over a capture of the "
2073                                "candidate's own window taken while the host was in "
2074                                "this appearance. Lines below the stated confidence "
2075                                "are not counted."
2076                            ),
2077                            "appearance_difference_basis": (
2078                                "The light capture and the dark capture were compared "
2079                                "pixel by pixel. A window that ignored the host "
2080                                "appearance, and a frozen or blank one, return the "
2081                                "same image twice and do not clear the stated "
2082                                "minimum."
2083                            ),
2084                            "method": (
2085                                "The host was driven into this appearance and the "
2086                                "candidate window was captured and read in it. The "
2087                                "host appearance is the precondition of this "
2088                                "observation, not the observation."
2089                            ),
2090                        },
2091                    ),
2092                    captured[name],
2093                ],
2094            )
2095
2096    if blocked:
2097        status = "incomplete"
2098    elif waived:
2099        status = "passed_with_waivers"
2100    else:
2101        status = "passed"
2102    report: dict[str, Any] = {
2103        "schema": OBSERVATIONS_SCHEMA,
2104        "candidate_digest": candidate_digest,
2105        "status": status,
2106        "manual_journey": entries["manual_journey"],
2107        "accessibility": entries["accessibility"],
2108        "waivers": sorted(waived),
2109    }
2110    canonical = dict(report)
2111    report["evidence_sha256"] = hashlib.sha256(
2112        json.dumps(canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
2113    ).hexdigest()
2114
2115    args.output.parent.mkdir(parents=True, exist_ok=True)
2116    args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
2117    performed = sum(
2118        1 for group in entries.values() for entry in group if entry["status"] == "passed"
2119    )
2120    print(
2121        json.dumps(
2122            {
2123                "schema": OBSERVATIONS_SCHEMA,
2124                "status": status,
2125                "performed": performed,
2126                "waived": sorted(waived),
2127                "required": len(MANUAL_JOURNEY) + len(ACCESSIBILITY),
2128                "blocked": blocked,
2129                "output": str(args.output),
2130            },
2131            indent=2,
2132        )
2133    )
2134    return 0 if status in {"passed", "passed_with_waivers"} else 1
2135
2136
2137if __name__ == "__main__":
2138    try:
2139        sys.exit(main())
2140    except ObservationError as error:
2141        print(f"error: {error}", file=sys.stderr)
2142        sys.exit(2)
2143
Served at tenant.openagents/omega Member data and write actions are omitted.