feat: DOM-selector benchmark arm — the incumbent comparison#17
Merged
Conversation
Adds openadapt_flow/benchmark/dom_arm.py — a steelman Playwright selector script run head-to-head against the compiled vision replay on the hybrid benchmark's frozen 20-slot schedule and the validation suite's perturbation modes, judged by the same OCR final-state identity check. Ports the hybrid branch's flag-gated MockMed drift hooks (notice/reqfield/modal-once/typelabel) and adds a new 'sort' mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
dom_arm now imports SCHEDULE / DRIFT_TYPES / note_for_slot / condition_url / verify_hybrid_final from hybrid_benchmark (merged to main in PR #14) instead of carrying duplicates; adds MockMed tests for the new sort drift mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
Results (all $0, deterministic): 14/20 tie on the frozen schedule (both arms stopped by notice/reqfield/modal-once); on the perturbation modes the DOM script wrote to the WRONG PATIENT on 4 of 8 modes (lookalike/missing/grow/sort, 8/8 runs) while the compiled arm's identity check safe-halted every one; DOM absorbed move/typelabel and is ~38x faster per clean run; compiled healed rename that broke the DOM script. Adds verification-dispute reporting: one theme run per arm completed but failed the shared OCR judge (dark-palette false negative, disclosed, counted as failure). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…cation Adversarial review of PR #17: the 8/8 positional-DOM wrong-action finding is an artifact of pairing a position-phrased task spec with an identity-keyed judge. This adds the identity-honest steelman as a THIRD arm (name-filtered: get_by_role row=Jane Sample -> Open) run across the full schedule and every perturbation mode. Three-arm result (all $0, deterministic): schedule 14/20 tie. On the perturbation matrix the name-filtered DOM completes CORRECTLY on lookalike/grow/sort (saved to #patient/p1), fails closed on missing, zero wrong actions — where the compiled arm safe-halted 8/8 with 0 heals, so on data drift the name-filtered arm finished the work the compiled arm declined. Positional DOM keeps its 8/8 wrong-patient writes (now scoped to positional selectors, not 'Playwright'). Verdict reframed to the honest finding: (a) spec underspecification is the wrong-action vector; (b) positional selectors silently retarget on data drift; (c) name-filtered DOM is safe AND available on data drift where a stable DOM exists — the compiled arm's remaining browser-side edges are demo-derived identity (no spec authoring), heal-through of label drift (rename), fail-closed semantics, plus non-DOM substrates. Retracts the asymmetric variant dismissal (the compiled identity band embeds the same patient name). Commits the two disputed theme finals; gitignores the rest of benchmark/dom/{finals,rows.jsonl}; Reproduce command now matches --n-per-perturbation 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
abrichr
added a commit
that referenced
this pull request
Jul 11, 2026
…ct stale-code finding The branch was originally cut from a stale local main that predated the identity-matcher fixes (#16/#17/#19), so the compiled arm ran against the pre-#16 matcher and recorded 3 sibling wrong-actions — a stale-code artifact. Rebased onto current main and re-ran the identity-sensitive cells: the compiled arm now safe-halts both the near-lexical sibling (Sorenson/Sorensen) and the decoy, 3/3 each — 0 identity wrong-actions. The browser identity fixes transfer to desktop-rendered OCR text. Narrative corrected in BENCHMARK.md; non-identity findings (UIA-tree gap, DPI/theme defeat vision but halt-not-miswrite, full prlctl automation) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
External criticism of this repo's benchmarks, which we accept: the real incumbent for "run the same GUI workflow 500 times" is not a computer-use agent — it is a DOM-selector script (Playwright/Selenium). Also $0, also fast, no OCR anywhere. Instead of arguing, this PR runs that experiment head-to-head against the compiled vision replay — same task, same frozen drift schedule, same arm-independent OCR + identity success check (
verify_hybrid_final, reused from the hybrid benchmark), wrong-action events measured for every arm.The task spec ("open the FIRST referral task") underdetermines exactly one selector, so both readings run as steelman arms: DOM (positional) = first row's Open button (literal reading); DOM (name-filtered) = the Open button in the row named
Jane Sample(identity reading, hardcoding the demonstrated patient exactly as the compiled arm's recorded identity band does). Full generated report:benchmark/dom/BENCHMARK.md.Three-arm perturbation matrix (n=2 each, deterministic, $0)
lookalike#patient/p0)missing#patient/p2)grow#patient/g1)sort#patient/p2)themerenamemovetypelabel* one
themerun per affected arm completed correctly (#patient/p1, right patient) but failed the dark-palette OCR judge — a judge false negative, disclosed, counted as a failure, and the two disputed final screenshots are committed alongside the report.Frozen 20-slot schedule: all three arms tie 14/20 (14 clean, 0/6 drifted — notice/reqfield/modal-once stop every arm). Wrong-action totals: compiled 0, DOM-positional 8, DOM-name-filtered 0. Maintenance totals: compiled 0, DOM-positional 8, DOM-name-filtered 10. Both DOM arms are ~36x faster per clean run (p50 0.2s vs 7.2s).
Verdict (verbatim from the generated report)
The wrong-action vector is spec underspecification, not "Playwright". On the frozen schedule all arms tie (compiled 14/20; dom 14/20; dom_named 14/20): the drift that halts the compiled replay (notice/reqfield/modal-once) stops both DOM scripts too. Both DOM scripts are ~36x faster per clean run (p50 0.2s vs 7.2s). The perturbation matrix is where the arms separate, and they separate by HOW EACH ARM NAMES ITS TARGET, not by DOM vs vision. (a) Positional selectors silently retarget under data drift. The positional script ("first row" — the literal reading of the task spec) wrote to the WRONG PATIENT on 4 of 8 modes (grow, lookalike, missing, sort; 8 runs, every one with a healthy-looking final screen). A position-phrased spec cannot notice that the row's identity changed. (b) The identity reading fixes it — and where a stable DOM exists, it also out-completes the compiled arm. The name-filtered script (same code, one selector keyed to the demonstrated patient) completed CORRECTLY on grow, lookalike, move, sort, typelabel and failed closed on missing, rename, with zero wrong actions. The compiled arm was equally safe (wrong actions: 0) but never healed to the true row on these modes — every data-drift outcome was a halt: on data drift the name-filtered DOM arm finished the work the compiled arm safely declined. (c) What demonstration buys: the identity came for free. Nobody had to DECIDE that "first referral" really means Jane Sample — the demonstration captured the target's identity as a matter of course, while the DOM arms needed that judgment hand-written into a selector (and the positional variant shows what happens when it is not). The compiled arm's remaining browser-side edges on this data: demo-derived identity with no spec authoring, heal-through of label drift (
renamebroke both DOM scripts' Open selector — a human edit each — while the ladder healed through it), and fail-closed halts with an accurate report. Its costs are equally plain: ~36x slower per run, and an OCR judge with failure modes of its own. (One condition —theme— produced judge-disputed verdicts: an OCR-judge artifact affecting both arms equally, not an automation difference; see the measurement-validity section.) Boundary, stated plainly: this comparison exists ONLY on browser backends. On desktop, VDI/Citrix, or any pixels-without-DOM substrate there is no selector script to write — the criticism's own point — and wherever a stable, accessible DOM exists, an identity-keyed selector script is the honest incumbent to beat: as fast as the positional one and, on this matrix, as safe as the compiled arm.What's in the PR
openadapt_flow/benchmark/dom_arm.py— both steelman scripts (positional + name-filtered, differing in exactly one documented selector), harness, outcome classification (success / halt-or-error / WRONG-ACTION / maintenance / verification-dispute), aggregation, three-column markdown renderer, three-arm chart with dispute footnote, orchestrator, CLI. Reuses the hybrid benchmark's frozenSCHEDULEandverify_hybrid_final.openadapt_flow/mockmed/static/app.js— flag-gatedsortdrift mode.benchmark/dom/{BENCHMARK.md, results.json, outcome_matrix.png}+ the two disputedthemefinals..gitignore— ignoresbenchmark/dom/{finals/,rows.jsonl}(disputed finals force-added).tests/test_dom_arm.py(24) incl. the one-selector-difference and identity-scoping cases +tests/test_mockmed.pysort-mode tests. Full suite green.Prior draft's asymmetric variant dismissal is retracted in the report: the compiled arm hardcodes the same patient constant in its recorded identity band; the arms differ in how identity is captured (demonstration vs hand-authored selector) and in fail-closed semantics, not in whether identity is encoded. The "writes to the wrong patient" framing is now scoped to positional selectors specifically. $0 spent: zero model calls anywhere.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ