summaryrefslogtreecommitdiff
path: root/scripts/windows_input/report.py
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-18 02:13:13 +0300
committerGitHub <noreply@github.com>2026-09-18 01:13:13 +0200
commitb88e8116e9e1f7e2f6e8b211536311cd17feca9a (patch)
tree699535f31233ae1aee63a110bdbf2e989ba0878b /scripts/windows_input/report.py
parent14351f121f6fc8c84b7c6c6eda78ad210e93cfcb (diff)
test: add local windows input gauntlet (#4298)
* test: add local windows input gauntlet refs #4251 * test: add local windows input gauntlet Harden elevation and channel identity checks, distinguish known host limitations, and surface incomplete cleanup. refs #4251 * test(windows): harden input gauntlet execution refs #4251 * test(windows): make input gauntlet self-contained refs #4251 * test(windows): bound input gauntlet failure paths refs #4251 * test(windows): extend input qualification gauntlet refs #4251 * test(windows): complete input qualification matrix refs #4251 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'scripts/windows_input/report.py')
-rw-r--r--scripts/windows_input/report.py446
1 files changed, 446 insertions, 0 deletions
diff --git a/scripts/windows_input/report.py b/scripts/windows_input/report.py
new file mode 100644
index 00000000..7c15c149
--- /dev/null
+++ b/scripts/windows_input/report.py
@@ -0,0 +1,446 @@
+"""Portable case catalogue and strict evidence verdicts for the local Windows gauntlet.
+
+This is not a host simulator. A report cannot turn absent Windows evidence into a pass.
+"""
+import argparse
+import json
+import re
+from pathlib import Path
+
+WIDTHS = [80, 119, 120, 121, 132, 160, 240]
+HEIGHTS = [24, 50]
+MODES = ["native", "legacy", "mok2", "kitty"]
+
+
+def hex_of(text):
+ return text.encode("utf-8").hex()
+
+
+def catalogue():
+ cases = []
+
+ def key(name, vk, text, modifiers=(), enhanced=None, legacy=None):
+ mask = (1 if 16 in modifiers else 0) | (2 if 18 in modifiers else 0) | (4 if 17 in modifiers else 0)
+ unicode = (vk + 32 if 65 <= vk <= 90 else vk if vk in (8, 9, 13, 27) else 0)
+ if 65 <= vk <= 90 and 16 in modifiers:
+ unicode = vk
+ if 17 in modifiers:
+ unicode = vk - 64 if 65 <= vk <= 90 else 10 if vk == 13 else 127 if vk == 8 else 0
+ expected = {"native": {"vk": vk, "modifiers": mask, "unicode": unicode, "modifier_keys": list(modifiers)}}
+ if text is not None:
+ expected.update({mode: {"hex": [hex_of(text)]} for mode in MODES[1:]})
+ if legacy is not None:
+ expected["legacy"] = {"hex": [hex_of(legacy)]}
+ if enhanced is not None:
+ expected["mok2"] = {"hex": [hex_of(f"\x1b[27;{mask + 1};{enhanced}~")]}
+ expected["kitty"] = {"hex": [hex_of(f"\x1b[{enhanced};{mask + 1}u")]}
+ cases.append(dict(id=name, kind="key", chords=[[*modifiers, vk]], expected=expected))
+
+ key("letter-a", 65, "a")
+ key("shift-letter", 65, "A", (16,))
+ key("enter", 13, "\r")
+ key("shift-enter", 13, None, (16,), 13)
+ cases[-1]["expected"]["legacy"] = {"loss": "enter", "hex": ["0d"]}
+ key("ctrl-enter", 13, None, (17,), 13)
+ cases[-1]["expected"]["legacy"] = {"loss": "modifier", "hex": ["0a"]}
+ key("ctrl-shift-enter", 13, None, (17, 16), 13)
+ cases[-1]["expected"]["legacy"] = {"loss": "modifier", "hex": ["0a"]}
+ key("tab", 9, "\t")
+ key("shift-tab", 9, "\x1b[Z", (16,))
+ cases[-1]["expected"]["mok2"] = {"hex": [hex_of("\x1b[27;2;9~")]}
+ cases[-1]["expected"]["kitty"]["hex"].append(hex_of("\x1b[9;2u"))
+ key("backspace", 8, "\x7f")
+ key("ctrl-backspace", 8, None, (17,), 127, "\x08")
+ key("alt-backspace", 8, None, (18,), 127, "\x1b\x7f")
+ key("escape", 27, "\x1b")
+ # Kitty's disambiguation flag encodes Escape distinctly.
+ cases[-1]["expected"]["kitty"] = {"hex": [hex_of("\x1b[27u")]}
+ for name, vk, seq, kitty in [("up", 38, "A", 57419), ("down", 40, "B", 57420), ("right", 39, "C", 57418),
+ ("left", 37, "D", 57417), ("home", 36, "H", 57423), ("end", 35, "F", 57424)]:
+ for prefix, modifiers, modifier in [("", (), 1), ("shift-", (16,), 2), ("ctrl-", (17,), 5), ("ctrl-shift-", (17, 16), 6)]:
+ if prefix == "ctrl-shift-" and name in ("up", "down", "home", "end"):
+ continue
+ key(prefix + name, vk, "\x1b[" + ("" if modifier == 1 else f"1;{modifier}") + seq, modifiers)
+ cases[-1]["expected"]["kitty"]["hex"].append(hex_of(f"\x1b[{kitty}{'' if modifier == 1 else f';{modifier}'}u"))
+ for name, vk, code, kitty in [("insert", 45, 2, 57425), ("delete", 46, 3, 57426),
+ ("page-up", 33, 5, 57421), ("page-down", 34, 6, 57422)]:
+ key(name, vk, f"\x1b[{code}~")
+ cases[-1]["expected"]["kitty"]["hex"].append(hex_of(f"\x1b[{kitty}u"))
+ for letter in "acdj lmqsu z".replace(" ", ""):
+ key("ctrl-" + letter, ord(letter.upper()), None, (17,), ord(letter), chr(ord(letter) - 96))
+ key("alt-v", 86, None, (18,), ord("v"), "\x1bv")
+ for name, text in [
+ ("paste-lf", "line 1\nline 2"),
+ ("paste-crlf", "line 1\r\nline 2\r\n"),
+ ("paste-cr", "line 1\rline 2"),
+ ("paste-whitespace", " one\t\n\n two \n"),
+ ("paste-unicode", "é e\u0301 日本語\nnext"),
+ ("paste-escape-looking", "literal [200~ and \\x1b[31m\nend"),
+ ]:
+ cases.append(dict(id=name, kind="paste", text=text, expected={mode: {"paste": text} for mode in MODES[1:]}))
+ cases.append(dict(id="mouse-interleave", kind="mouse-interleave", text="mouse\npaste",
+ expected={mode: {"mouse_interleave": True} for mode in MODES[1:]}))
+ cases.append(dict(id="mouse-focus-refresh", kind="mouse-focus-refresh",
+ expected={mode: {"mouse_focus_refresh": True} for mode in MODES[1:]}))
+ transitions = "a\r" + "b\x1b[27;2;13~" + "c\x1b[13;2u" + "d\x1b[27;2;13~" + "e\r" + "f"
+ cases.append(dict(id="mode-transitions", kind="mode-transitions",
+ expected={mode: {"hex": [hex_of(transitions)]} for mode in MODES[1:]}))
+ cases.append(dict(id="dead-acute", kind="layout-key", dead="´", base_vk=69,
+ expected={mode: {"hex": [hex_of("é")]} for mode in MODES[1:]}))
+ for name, prompt, text in [
+ ("dead-grave", "With US-International active, type grave then e, once; no Enter.", "è"),
+ ("dead-circumflex", "With US-International active, type circumflex then e, once; no Enter.", "ê"),
+ ("dead-tilde", "With US-International active, type tilde then n, once; no Enter.", "ñ"),
+ ("dead-diaeresis", "With US-International active, type diaeresis then u, once; no Enter.", "ü"),
+ ("dead-space", "With US-International active, type acute then Space, once; no Enter.", "'"),
+ ("altgr-euro", "With your declared euro-producing AltGr layout, type € using its AltGr chord, once; no Enter.", "€"),
+ ("ime-commit", "Using your declared IME, compose and commit 日本語 (not paste); do not submit the prompt.", "日本語"),
+ ]:
+ cases.append(dict(id=name, kind="manual", prompt=prompt, expected={mode: {"hex": [hex_of(text)]} for mode in MODES[1:]}))
+ # Explicit qualification tasks, never auto-passed by key/byte checks.
+ for name, prompt in [
+ ("dead-cancel-repeat", "Qualify dead-key cancellation, repeated accent and non-composing next character against the direct-host baseline."),
+ ("ime-cancel", "Qualify IME cancel and partial composition without committed/duplicate text."),
+ ("native-repeat-release", "Qualify held-key repeat, release identity, and modifier interleaving using native records."),
+ ("locks-keypad", "Qualify CapsLock/NumLock, keypad Enter/operators/decimal and restore lock states."),
+ ("mouse-right-edge", "Click/drag/wheel at a marked pane column above 120; compare actual report coordinates and visual target."),
+ ("wrap-rendering", "Verify the displayed ruler and wrapped multiline text at both window heights and every observed width."),
+ ("capture-refresh", "Toggle mouse capture/config reload, refocus, detach/reattach; repeat paste and Shift+Enter."),
+ ("setup-recovery", "Inject a recoverable setup failure and late VT activation; verify mode restoration and the same input sentinels."),
+ ("ctrl-v", "Qualify raw Ctrl+V only with the Windows Terminal paste binding explicitly removed."),
+ ("alt-enter", "Qualify Alt+Enter with the Windows Terminal fullscreen binding explicitly controlled."),
+ ("ctrl-shift-up", "Qualify Ctrl+Shift+Up with the Windows Terminal scroll binding explicitly controlled."),
+ ("ctrl-shift-down", "Qualify Ctrl+Shift+Down with the Windows Terminal scroll binding explicitly controlled."),
+ ("ctrl-shift-home", "Qualify Ctrl+Shift+Home with the Windows Terminal scroll binding explicitly controlled."),
+ ("ctrl-shift-end", "Qualify Ctrl+Shift+End with the Windows Terminal scroll binding explicitly controlled."),
+ ("paste-supplementary", "Qualify supplementary-plane clipboard text, including emoji, against the direct-host baseline."),
+ ("paste-burst", "Qualify a 200-line clipboard burst with the host multiline-paste warning configured or handled explicitly."),
+ ("clipboard-nontext", "Qualify supported image/file clipboard integrations separately; do not infer from text paste."),
+ ]:
+ cases.append(dict(id=name, kind="qualification", prompt=prompt, expected={}))
+ return dict(schema=1, widths=WIDTHS, heights=HEIGHTS, modes=MODES, cases=cases)
+
+
+def verdict(case, mode, evidence):
+ """The full capture, not a matching prefix, is the primary assertion."""
+ if evidence.get("status") in {"not_run", "unsupported", "inconclusive"}:
+ return evidence["status"], evidence.get("reason", "No qualifying observation")
+ expected = case["expected"].get(mode)
+ if expected is None:
+ return "not_run", "No automatic oracle for this case/profile; requires qualification"
+ if not evidence.get("ready") or not evidence.get("focus_verified") or not evidence.get("complete"):
+ return "inconclusive", "Missing readiness, focus, or complete capture"
+ if evidence.get("error"):
+ return "inconclusive", evidence["error"]
+ if evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down") and "vk" not in expected:
+ expected = {"hex": [""]} # Plain page keys intentionally control Herdr's host scrollback.
+ def geometry(name):
+ value = evidence.get(name)
+ return value[:2] if isinstance(value, list) and len(value) >= 2 and all(type(v) is int for v in value[:2]) else None
+ outer, pane = geometry("outer_geometry"), geometry("pane_geometry")
+ final_pane, final_outer = geometry("final_pane_geometry"), geometry("final_outer_geometry")
+ if outer != [evidence.get("width"), evidence.get("height")]:
+ return "inconclusive", "Requested geometry was not observed"
+ if pane is None or min(pane) <= 0:
+ return "inconclusive", "Missing actual pane dimensions"
+ if final_pane != pane:
+ return "inconclusive", "Pane geometry changed during capture"
+ if final_outer != outer:
+ return "inconclusive", "Outer geometry changed during capture"
+ if evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down") and "vk" in expected:
+ records = evidence.get("records")
+ if not isinstance(records, list) or any(not isinstance(record, list) or len(record) != 7
+ or not all(type(field) is int for field in record) for record in records):
+ return "inconclusive", "Malformed native record"
+ return (("inconclusive", "No positive host-scrollback evidence for the consumed page key")
+ if all(record[0] in (4, 16) for record in records)
+ else ("fail", "Plain page key unexpectedly reached the pane"))
+ if (evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down")
+ and evidence.get("hex") == ""):
+ return "inconclusive", "No positive host-scrollback evidence for the consumed page key"
+ if "vk" in expected:
+ records = evidence.get("records")
+ scans = evidence.get("scans", [])
+ chord = case["chords"][0]
+ if not isinstance(records, list):
+ return "inconclusive", "Malformed native record"
+ if not isinstance(scans, list) or len(scans) != len(chord) or not all(type(scan) is int for scan in scans):
+ return "inconclusive", "Missing injected scan-code evidence"
+ expected_scans = dict(zip(chord, scans))
+ aliases = {160: 16, 161: 16, 162: 17, 163: 17, 164: 18, 165: 18}
+ held = set()
+ keys = []
+ for record in records:
+ if not isinstance(record, list) or len(record) != 7 or not all(type(field) is int for field in record):
+ return "inconclusive", "Malformed native record"
+ if record[0] in (4, 16): # Resize/focus notifications carry no typed text.
+ continue
+ if record[0] != 1:
+ return "fail", "Unexpected non-key native input"
+ vk = aliases.get(record[3], record[3])
+ if vk not in (16, 17, 18):
+ keys.append(record)
+ continue
+ if vk not in expected["modifier_keys"] or record[5] != 0 or record[2] != 1 or record[4] != expected_scans[vk]:
+ return "fail", "Unexpected/corrupted modifier record"
+ if record[1] == 1 and vk not in held:
+ held.add(vk)
+ elif record[1] == 0 and vk in held:
+ held.remove(vk)
+ else:
+ return "fail", "Unbalanced modifier sequence"
+ if held:
+ return "fail", "Modifier release missing"
+ if len(keys) != 2 or [r[1] for r in keys] != [1, 0]:
+ return "fail", "Expected exactly one non-modifier down/up pair"
+ for record in keys:
+ control = record[6]
+ modifiers = bool(control & 16) + 2 * bool(control & 3) + 4 * bool(control & 12)
+ if (record[3] != expected["vk"] or modifiers != expected["modifiers"] or record[2] != 1
+ or record[5] != expected["unicode"] or record[4] != expected_scans[expected["vk"]]):
+ return "fail", "Native identity/modifiers/repeat differ"
+ return "pass", "Native down/up pair matches"
+ try:
+ raw = bytes.fromhex(evidence["hex"])
+ except (KeyError, ValueError, TypeError):
+ return "inconclusive", "Missing or malformed raw bytes"
+ if expected.get("mouse_interleave"):
+ motion = rb"(?:\x1b\[<35;\d+;\d+M)+"
+ newline = rb"(?:\r\n|\r|\n)"
+ pattern = b"a" + motion + rb"\x1b\[200~mouse" + newline + rb"paste\x1b\[201~" + motion + b"b"
+ return ("pass", "Typing, mouse motion and paste remained ordered") if re.fullmatch(pattern, raw) else ("fail", "Mouse interleave order or payload differs")
+ if expected.get("mouse_focus_refresh"):
+ coords = rb"\d+;\d+"
+ gesture = (rb"(?:\x1b\[<35;" + coords + rb"M)*"
+ + rb"\x1b\[<0;" + coords + rb"M"
+ + rb"\x1b\[<0;" + coords + rb"m"
+ + rb"\x1b\[<64;" + coords + rb"M")
+ pattern = b"a" + gesture + b"b" + b"c" + gesture + b"d"
+ return ("pass", "Click/wheel reporting survived focus loss and regain") if re.fullmatch(pattern, raw) else ("fail", "Mouse reporting failed before or after focus regain")
+ if case["id"] == "mode-transitions" and evidence.get("path") == "direct":
+ legacy_only = hex_of("a\rb\rc\rd\re\rf")
+ kitty_only = hex_of("a\rb\rc\x1b[13;2u" + "d\re\rf")
+ if raw.hex() in {legacy_only, kitty_only}:
+ return "unsupported", "Direct Windows Terminal ignored modifyOtherKeys during the transition chain"
+ return "fail", "Unexpected direct-host runtime transition bytes"
+ if "loss" in expected:
+ return ("unsupported", "Plain VT cannot preserve modified Enter") if raw.hex() in expected["hex"] else ("fail", "Unexpected plain-VT modified Enter result")
+ if "hex" in expected:
+ return ("pass", "Exact bytes match") if raw.hex() in expected["hex"] else ("fail", "Bytes differ (including any duplicates/trailing input)")
+ if not raw.startswith(b"\x1b[200~") or not raw.endswith(b"\x1b[201~"):
+ return "fail", "Missing bracketed-paste envelope; newlines may be key events"
+ try:
+ actual = raw[6:-6].decode("utf-8")
+ except UnicodeDecodeError:
+ return "fail", "Paste is not intact UTF-8"
+ # Line-ending conversion is expected on Windows; all other payload bytes matter.
+ normalize = lambda text: text.replace("\r\n", "\n").replace("\r", "\n")
+ return ("pass", "One complete paste matches") if normalize(actual) == normalize(expected["paste"]) else ("fail", "Paste payload differs")
+
+
+def known_host_gap(observation, case, terminal_version):
+ """A narrowly observed host limitation, never a blanket version exemption."""
+ legacy = case["expected"].get("legacy", {}).get("hex", [])
+ mok2 = case["expected"].get("mok2", {}).get("hex", [])
+ return (observation.get("path") == "direct"
+ and observation.get("mode") == "mok2"
+ and case["id"] in {"shift-enter", "ctrl-enter", "ctrl-shift-enter", "shift-tab"}
+ and re.match(r"^1\.(?:24|25)\.", str(terminal_version)) and set(legacy) != set(mok2)
+ and observation.get("hex") in legacy)
+
+
+def channel_identity_errors(hosts):
+ """Compare both launcher identities and the processes actually activated."""
+ identities = {}
+ for host in hosts:
+ values = identities.setdefault(host.get("channel"), set())
+ for field, kind in (("launcher_identity", "file"), ("installation_identity", "installation")):
+ if host.get(field):
+ values.add((kind, host[field]))
+ for run in host.get("runs", []):
+ for field, kind in (("image_identity", "file"), ("installation_identity", "installation"), ("process_identity", "process")):
+ if run.get(field):
+ values.add((kind, run[field]))
+ return ["Stable and Preview share a Terminal executable, installation, or process identity"] if identities.get("stable", set()) & identities.get("preview", set()) else []
+
+
+def summarize(document):
+ matrix = catalogue()
+ cases = {case["id"]: case for case in matrix["cases"]}
+ rows = []
+ seen = set()
+ captures = set()
+ for observation in document.get("observations", []):
+ identity = tuple(observation.get(k) for k in ("host", "path", "mode", "phase", "width", "height", "case"))
+ if identity in seen:
+ raise ValueError(f"Duplicate observation identity: {identity}")
+ seen.add(identity)
+ case = cases[observation["case"]]
+ status, reason = verdict(case, observation["mode"], observation)
+ scope = "direct_host" if observation.get("path") == "direct" else "through_herdr_not_yet_attributed"
+ if status in ("pass", "fail"):
+ bound = [run for host in document.get("hosts", []) if host.get("channel") == observation.get("host")
+ for run in host.get("runs", [])
+ if run.get("nonce") == observation.get("nonce") and run.get("path") == observation.get("path")
+ and run.get("mode") == observation.get("mode") and run.get("pid", 0) > 0 and run.get("hwnd", 0) != 0
+ and run.get("elevated") is False and run.get("image_identity") and run.get("installation_identity")]
+ capture = observation.get("capture_id")
+ if len(bound) != 1 or document.get("controller_elevated") is not False or not capture or capture in captures:
+ status, reason = "inconclusive", "Missing non-elevated owned-run binding or fresh capture identity"
+ else:
+ captures.add(capture)
+ if status == "fail" and known_host_gap(observation, case, bound[0].get("terminal_version")):
+ status, reason = "unsupported", "Direct host ignored mOK and emitted the case's legacy bytes"
+ rows.append({**observation, "status": status, "reason": reason, "failure_scope": scope})
+ counts = {status: sum(r["status"] == status for r in rows) for status in ("pass", "fail", "not_run", "unsupported", "inconclusive")}
+ # No run may claim all-green just because it produced zero/missing observations.
+ planned = set()
+ geometries = [(120, 30, True)] + [(w, h, False) for h in document.get("heights", HEIGHTS) for w in document.get("widths", WIDTHS)] + [(80, 30, False)]
+ selected_cases = document.get("cases") or list(cases)
+ for host in document.get("channels") or ("stable", "preview"):
+ for path in document.get("paths") or ("direct", "herdr"):
+ for mode in document.get("modes", MODES):
+ for phase, (width, height, full) in enumerate(geometries, 1):
+ for case_id in selected_cases if full else (case_id for case_id in ("letter-a", "shift-enter", "paste-lf", "mouse-focus-refresh") if case_id in selected_cases):
+ planned.add((host, path, mode, phase, width, height, case_id))
+ missing = planned - seen
+ if seen - planned:
+ raise ValueError("Observations outside the declared run matrix")
+ hosts = document.get("hosts", [])
+ errors = list(document.get("errors", [])) + channel_identity_errors(hosts)
+ expected_hosts = set(document.get("channels") or ("stable", "preview"))
+ complete = (bool(rows) and not missing and {h.get("channel") for h in hosts} == expected_hosts
+ and all(h.get("runs") for h in hosts)
+ and not errors and not document.get("cleanup_errors")
+ and all(r["status"] == "pass" for r in rows))
+ return {**document, "errors": errors, "observations": rows, "counts": counts, "coverage_missing": len(missing), "observed_checks_passed": complete,
+ "native_qualification": "Required; this report is not a full Windows support certificate"}
+
+
+def qualification_matrix(result):
+ """Collapse real observations into the user-facing capability summary."""
+ rows = result.get("observations", [])
+ required_channels = set(result.get("channels") or ("stable", "preview"))
+ manual_cases = {case["id"] for case in catalogue()["cases"] if case["kind"] == "manual"}
+ groups = [
+ ("Printable keys", {"letter-a", "shift-letter"}, None),
+ ("Shift+Enter", {"shift-enter"}, None),
+ ("Ctrl+Enter", {"ctrl-enter"}, None),
+ ("Ctrl+Shift+Enter", {"ctrl-shift-enter"}, None),
+ ("Navigation/editing", {"up", "down", "left", "right", "home", "end", "insert", "delete", "tab", "shift-tab", "page-up", "page-down"}, None),
+ ("Multiline paste", {"paste-lf"}, None),
+ ("CR/LF/CRLF paste", {"paste-lf", "paste-crlf", "paste-cr"}, None),
+ ("Unicode/whitespace paste", {"paste-unicode", "paste-whitespace"}, None),
+ ("Paste framing/ordering", {case["id"] for case in catalogue()["cases"] if case["kind"] == "paste"}, None),
+ ("Resize 120 -> 80", {"letter-a", "shift-enter", "paste-lf"}, 80),
+ ("Mouse while typing/pasting", {"mouse-interleave"}, None),
+ ("Mouse after focus regain", {"mouse-focus-refresh"}, None),
+ ("Mouse after resize", {"mouse-focus-refresh"}, 80),
+ ("Dead-key composition", {"dead-acute"}, None),
+ ("AltGr", {"altgr-euro"}, None),
+ ("IME composition", {"ime-commit"}, None),
+ ("Runtime mode transitions", {"mode-transitions"}, None),
+ ]
+
+ def cell(case_ids, width, path, modes):
+ matched = [row for row in rows if row.get("case") in case_ids and row.get("path") == path
+ and row.get("mode") in modes and (width is None or row.get("width") == width)]
+ statuses = {row.get("status") for row in matched}
+ if "fail" in statuses:
+ return "FAIL"
+ if case_ids <= manual_cases and not any(status == "pass" for status in statuses):
+ return "MANUAL"
+ if {row.get("case") for row in matched} != case_ids:
+ return "PARTIAL" if "pass" in statuses else "INCONCLUSIVE" if "inconclusive" in statuses else "NOT TESTED"
+ if required_channels and {(row.get("host"), row.get("case")) for row in matched} != {
+ (channel, case_id) for channel in required_channels for case_id in case_ids}:
+ return "PARTIAL"
+ if case_ids == {"mode-transitions"} and path == "direct" and statuses <= {"unsupported", "inconclusive"}:
+ return "X - mOK ignored"
+ if path == "direct" and modes == {"legacy"} and statuses == {"unsupported"}:
+ return "X - becomes Enter" if case_ids == {"shift-enter"} else "X - loses modifier"
+ def case_passed(case_id):
+ statuses_for_case = {row.get("status") for row in matched if row.get("case") == case_id}
+ return "pass" in statuses_for_case or (width == 80 and path == "direct" and modes == {"legacy"}
+ and case_id == "shift-enter" and statuses_for_case == {"unsupported"})
+ per_case_passed = all(case_passed(case_id) for case_id in case_ids)
+ allowed = {"pass", "inconclusive"}
+ if width == 80 and path == "direct" and modes == {"legacy"}:
+ allowed.add("unsupported")
+ if per_case_passed and statuses <= allowed:
+ return "PASS**" if "inconclusive" in statuses else "PASS"
+ if "inconclusive" in statuses:
+ return "INCONCLUSIVE"
+ if "unsupported" in statuses:
+ return "UNSUPPORTED"
+ return "NOT TESTED"
+
+ table = []
+ for name, case_ids, width in groups:
+ herdr_modes = {"legacy"} if name in {"Mouse after resize", "Runtime mode transitions"} else {"mok2"} if "Enter" in name or name in {"Resize 120 -> 80", "Dead-key composition", "AltGr", "IME composition"} else {"legacy"}
+ table.append((name, cell(case_ids, width, "herdr", herdr_modes),
+ cell(case_ids, width, "direct", {"legacy"}), cell(case_ids, width, "direct", {"kitty"})))
+ return table
+
+
+def herdr_protocol_label(result):
+ runs = {(host.get("channel"), run.get("path"), run.get("mode"), run.get("nonce"))
+ for host in result.get("hosts", []) for run in host.get("runs", [])
+ if run.get("path") == "herdr" and run.get("nonce")}
+ proven = {(row.get("host"), row.get("path"), row.get("mode"), row.get("nonce"))
+ for row in result.get("observations", [])
+ if row.get("path") == "herdr" and row.get("input_reader") == "windows-console"
+ and row.get("input_transport") == "win32-serialized" and row.get("nonce")}
+ return "Win32 (Herdr)*" if runs and runs <= proven else "Herdr default (UNKNOWN)*"
+
+
+def print_qualification_matrix(result):
+ table = [("Thing", herdr_protocol_label(result), "Plain VT", "Kitty"), *qualification_matrix(result)]
+ widths = [max(len(str(row[column])) for row in table) for column in range(4)]
+ line = lambda row: " | ".join(str(value).ljust(widths[index]) for index, value in enumerate(row))
+ print("\nWindows input qualification results")
+ print(line(table[0]))
+ print("-+-".join("-" * width for width in widths))
+ for row in table[1:]:
+ print(line(row))
+ print("* Herdr protocol label comes from runtime evidence; UNKNOWN is never treated as Win32.")
+ print("** At least one capable host passed; host capability gaps remain visible in report.json.")
+ print("MANUAL requires an operator-assisted -Manual run; no automated result is claimed.")
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("command", choices=["matrix", "report"])
+ parser.add_argument("--input", type=Path)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = catalogue() if args.command == "matrix" else summarize(json.loads(args.input.read_text(encoding="utf-8-sig")))
+ args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+ if args.command == "report":
+ counts = result["counts"]
+ through_failures = sum(row["status"] == "fail" and row.get("path") == "herdr" for row in result["observations"])
+ direct_failures = sum(row["status"] == "fail" and row.get("path") == "direct" for row in result["observations"])
+ print(f"Observed: {counts['pass']} pass, {counts['fail']} fail, {counts['unsupported']} unsupported, "
+ f"{counts['inconclusive']} inconclusive, {counts['not_run']} not run; "
+ f"{result['coverage_missing']} planned rows missing")
+ if through_failures:
+ print(f"Assessment: {through_failures} through-Herdr failures need attribution; inspect report.json before treating them as product regressions")
+ elif result.get("errors") or result.get("cleanup_errors"):
+ print("Assessment: harness or cleanup failed; this run does not qualify input behavior")
+ elif direct_failures:
+ print(f"Assessment: {direct_failures} direct-host differences observed; these are not automatically Herdr bugs")
+ elif counts["inconclusive"] or counts["not_run"] or counts["unsupported"] or result["coverage_missing"]:
+ print("Assessment: observed assertions passed, but host limitations or qualification gaps remain")
+ else:
+ print("Assessment: all planned observed assertions passed")
+ if result.get("errors"):
+ print("Run errors: " + " | ".join(result["errors"]))
+ if result.get("cleanup_errors"):
+ print("Cleanup errors: " + " | ".join(result["cleanup_errors"]))
+ print_qualification_matrix(result)
+ return 1 if result["counts"]["fail"] or result.get("errors") or result.get("cleanup_errors") else 2 if not result["observed_checks_passed"] else 0
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())