feat(replay): recorded landmark identity for wait, is coverage — read-only step identity (#1349)#1381
feat(replay): recorded landmark identity for wait, is coverage — read-only step identity (#1349)#1381thymikee wants to merge 5 commits into
Conversation
…c/replay Move buildIndexMap/buildAncestryChain/filterIdentitySet out of the daemon's session-target-evidence into the shared replay zone so the commands runtime (wait's polling loop, #1349) can consume them without importing the daemon; press-retarget drops its private buildIndexMap duplicate.
…attern coverage for is (#1349) - New CommandDescriptor trait targetIdentityVerification pins the evidence-carrying command set and routes wait to a post-resolution phase so an annotated wait never enters the generic pre-dispatch verification (an absent landmark is its expected starting condition). - wait <selector> records landmark-mode target-v1 evidence (existence self-check; identity-empty matches record no annotation) and, on replay, keeps polling until a selector match carries the recorded identity; a deadline with only impostor matches fails closed as an identity-mismatch REPLAY_DIVERGENCE, a recorded-unverifiable annotation refuses before polling, and a plain timeout stays an action-failure divergence. - is (except exists) joins the get pattern: evidence at record time, generic pre-dispatch verification, and the post-resolution guard threaded through dispatch; direct-iOS fast paths for wait/is are gated during recording and guarded replays. - Read-only find stays intentionally unannotated (fuzzy-locator resolution has no selector-chain identity token), proven by test.
…ark identity (#1349) A qualifying ADR 0016 guard is now a selector wait whose target-v1 annotation is verified; identity-less or unverifiable guards are refused with a recovery hint. Adds the reshuffled-screen false-pass regression: record -> publish -> replay against a same-label/different-ancestry tree diverges as identity-mismatch (matchCount >= 1 proving the selector alone would have false-passed).
…m evidence-writer complexity Shared buildPostDispatchIdentityMismatchResponse behind the guard and wait-landmark conversions; extracted payload-ceiling helpers from computeTargetEvidence; identity-refusal conversion split out of resolveReplayStepResponse. Docs: ADR 0012 decision 3 amendment (#1349), ADR 0016 guard strengthening, help workflow/save-script text.
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
…xplicit, trim ADR-restating docs The landmark-mode self-check was provably a tautology (the winner is a member of its own identity set whenever the parent walk is intact), so a membership scan defended only by a comment is replaced with the explicit decision: broken walk fails closed, landmark is verified by construction, action mode keeps decision 3's step-5 self-check. Doc comments that re-argued the ADR amendment now state behavior and point to it.
|
Reviewed exact head
Descriptor phase → replay verifier → daemon-only landmark guard → snapshot-polling wait → bounded divergence conversion otherwise looks correct: impostors keep polling, ordinary timeout remains intact, and guarded recording/replay disables direct iOS fast paths as intended. Exact-head CI is green and merge state is clean. No ready label. |
Closes #1349.
What
Extends ADR 0012's
target-v1identity verification to the eligible read-only steps without changing their execution semantics, and strengthens ADR 0016's destination guard from selector existence to recorded landmark identity. Four commits, sequenced dependency-first:refactor(replay): shared tree helpers — moves the pure evidence primitives (buildIndexMap/buildAncestryChain/filterIdentitySet) from the daemon intosrc/replay/target-evidence-tree.tsso the commands-layer wait loop can consume them (commands → replaywas already a legal DAG edge); deletespress-retarget.ts's privatebuildIndexMapduplicate.feat(replay): wait landmark verification +iscoverage — the core.feat(publication): guard requires verified evidence + the reshuffled-screen false-pass regression.refactor(replay): dedupe/complexity cleanup shaken out by the fallow gates, plus ADR/help docs.Design decisions a reviewer should check
.adformat: what's new istargetIdentityVerification: 'pre-dispatch' | 'post-resolution'onCommandDescriptor, pinning the evidence-carrying set ({click, press, longpress, fill, get, is} → pre-dispatch; wait → post-resolution) with a parity test. An annotated wait therefore structurally cannot enter the generic pre-action path — the issue's key acceptance criterion. Only ADR 0012 path 1 (recorded-unverifiable, which consults no screen) still refuses a wait before polling.identity-mismatchREPLAY_DIVERGENCE(matchCountfrom the last matching poll), while a never-matched selector stays the plain timeout (action-failure→ state-repair routing, unchanged). Positional signals (sibling/viewportOrder) are deliberately not compared — a guard proves the landmark exists, not that it kept its list position, and comparing position would false-fail legitimately reshuffled-but-correct screens.computeTargetEvidence(..., {mode:'landmark'})self-checks identity-set membership (mirroring what replay verifies); an identity-empty match (no id, no label after Android replay: non-unique framework idandroid:id/titleused as primary identity → systematic false divergences (FDR 1.0) #1269 demotion) records no annotation — fail-open to today's semantics, and publication then refuses it as a guard with a hint naming a labeled landmark.is(exceptexists) = the exactgetpattern: evidence from its resolved node, generic pre-dispatch verification,assertExpectedResolvedTargetguard threaded through dispatch. Direct-iOSwait/isfast paths are gated during recording and guarded replays (same rulegethas had since feat: parse and preserve .ad target-v1 evidence (ADR 0012 migration step 3) #1196; authoring-only cost).is exists, all read-onlyfindvariants (fuzzy-locator resolution has no selector-chain identity token for the classifier),wait text/stable/duration/@ref(wait @refstays rejected by publication, never converted).verification: "verified".Wire/plumbing
The recorded landmark travels the daemon-only
internal.replayLandmarkGuardchannel (unspoofable, same asreplayTargetGuard); the loop's deadline refusal isreason: wait_landmark_identity_mismatchwith a compact{matchCount, observed, observedAncestry}payload, converted by the step loop into the standard bounded divergence (shared shaping with the guard-mismatch path viabuildPostDispatchIdentityMismatchResponse).node/preActionNodesare stripped before bothsession.actionsand the wire on every path.Validation
isrecord/verify/guard, read-onlyfind+is existsdeferral proofs.matchCountomitted; loop refusal →identity-mismatchdivergence withrepairHint: caution; duration-wait annotation inert.session save-script(annotation lands in the.ad) → replay against a same-label/different-ancestry tree →REPLAY_DIVERGENCE identity-mismatchwithmatchCount ≥ 1(the proof the selector alone would have false-passed).Perf / size
No measurable regression: live unannotated wait polls differ by one small allocation; the annotated-replay identity check is O(tree) per poll, dwarfed by the poll's own snapshot capture. No new startup-parse weight (the commands bundle already reached the identity modules; the new shared module is ~80 dependency-free lines). The one deliberate cost: recording sessions use the snapshot path for
wait/isinstead of the direct-iOS runner query, which evidence capture requires.Docs: ADR 0012 decision-3 amendment (#1349) + migration row 11, ADR 0016 amendment,
help workflow/session save-scripttext.