mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
t3code/cleanup-claim-tests
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba6c818d81 |
spike(daemon): give the ADR-0014 ref frame private ownership (#2296)
* refactor(daemon): make the ADR 0014 ref frame one owned value
The four `refFrame*` fields on `SessionState` were policed only by the R7
ownership table: any daemon module could write them, and only a full-graph AST
scan could say whose write it was. They are now one `RefFrame` value whose brand
key is private to `src/daemon/ref-frame.ts`, so a module outside that file cannot
construct one and cannot edit the one a session holds; the transitions replace it
whole. Every transition, rejection reason and epoch rule is unchanged.
Readers moved to the accessors ref-frame.ts exports (`refFrameState`,
`refFrameScope`, `refFrameEpoch`, plus a new `refFrameTree` and `refFrame`).
`internal-observation.ts` drops its four-field lineage copy and its field-by-field
comparison: frame identity is now one `===`.
Seen red: with the empty-result early return removed from
`markSessionPartialRefsIssued`, the new frame-identity assertion in
session-snapshot.test.ts fails; restored, it passes. A planted foreign writer
module was rejected by tsc (TS2741 missing brand, TS2540 read-only property)
before deletion.
* docs(depgraph): note the ref frame outgrew its R7 row
* refactor(daemon): make the ref frame nominal, not symbol-branded
A symbol brand on a plain object type stops construction from nothing, but not
`{ ...refFrame(session), state: 'active' }`: object spread copies the symbol key,
so any daemon module could mint an incoherent frame (active state, stale tree)
out of a coherent one and it type-checked. Proven before the fix with a throwaway
module doing exactly that write: tsc reported nothing.
The frame is now a class with `#`-private fields behind getters. That makes the
type nominal, so no object literal is assignable to it — the same probe now fails
with TS2739 (`missing #fields, scope, generation, expired`). Construction stays
inside ref-frame.ts, and the four claim sites (ADR 0014, the SessionState field
doc, and the two in the R7 owner table) now say what the type does and does not
judge: it cannot see a whole frame moved unchanged, which is why the R7 row stays.
Expiry is idempotent by identity again. `expired()` returns THIS frame when the
frame is already expired, rather than an equal copy, which is what the lineage
check in internal-observation.ts compares with `===`. Seen red: with that early
return removed, the tightened ref-frame test fails with "Values have same
structure but are not reference-equal"; green with it.
Also: the ADR 0014 stale-ref help sample seeds its epoch through a real frame
activation again, instead of leaning on the pre-frame snapshotGeneration
fallback, and a find test drops a `?? []` that can no longer be reached.
Behavior is unchanged: same frame contents, same transitions, same admission.
* chore(gates): collapse the four ADR 0014 R7 rows into the owned refFrame value
R7's owner table listed `refFrameState`, `refFrameScope`, `refFrameTree` and
`refFrameGeneration` as four fields that had to be written together by one
module; the code now carries them as one nominal value, so the table carries one
row. R10 follows: 19 writer-owned fields to 16, 22 owner claims to 19.
The row itself stays. The type stops construction, editing and spread-derivation
of a frame outside ref-frame.ts, but it cannot judge a whole frame moved
unchanged — clearing the field, or assigning another session's frame — and the
table can. The comments say that rather than claiming full enforcement.
Seen red: a planted `session.refFrame = undefined` in snapshot-session.ts fails
R7 with "owned by src/daemon/ref-frame.ts"; green once reverted.
* style: apply oxfmt
* refactor(daemon): keep ref-frame expiry module-private
`RefFrame` exposed a public `expired()` method, so any module holding a
frame could derive a new valid one and install it through a reconstructed
session record, past the R7 field scan. Expiry is now a static on the
unexported class, reachable only inside ref-frame.ts; the frame's surface is
four getters. A type-level regression pins that no outside module can
construct, spread, edit, or derive a frame (tsc covers src tests, so a
directive that stops erroring fails typecheck).
* test(daemon): hold the three accessor migrations within the size ratchet
Each file grew by exactly its new ref-frame import; one blank line between
mock blocks goes so the files stay at their merge-base length.
|
||
|
|
52402aec4d |
refactor(daemon): split touch interaction orchestration into semantic modules (#1748)
* refactor(daemon): split touch interaction orchestration Closes part of #1691: interaction-touch.ts becomes a router; press, fill, direct-iOS, shared runtime, Android readiness, and response projection each own one module. Behavior is unchanged. * test(daemon): split touch interaction coverage by module Redistributes all 87 discovered cases across the new module topology and re-keys the two touch-family fallow baseline entries to the paths that now hold the same (net one fewer) findings. * docs: point ADR 0014 at the merged Android readiness regression file * refactor(daemon): give targeted-touch admission its own module Keeps interaction-touch-press.ts inside the 300-line budget after the complexity decomposition: admission (surface/capability/button policy, target parsing, @ref staleness and mutation admission) answers its own question. * refactor(daemon): drop the redundant targeted-touch label alias * test(daemon): split touch suites along the new production seams Adds the press-admission suite the production module was missing and splits the four over-budget suites along new production seams (direct-iOS eligibility, Android ref freshness, touch payload). Every suite installs the full device mock set: three Android-session cases regressed to TOOL_MISSING on a runner without adb when the mock set was trimmed per file. |
||
|
|
152894cce2 |
docs(adr): rules-first ADR restructure + ADR 0017 proposal (unified event journal) (#1399)
* docs(adr): rules-first restructure of 0012/0014/0016, drop completed migration logs ADR 0012 alone was 42% of the ADR corpus by bytes; consulting it cost ~28k tokens of mostly process history. Restructure per the new shape convention (added to the ADR README): Status + a normative 'Rules at a glance' first so a reader can stop after ~50 lines, rationale and refuted alternatives kept below the fold, and completed migration plans/landing tables deleted — git history is the archive. - 0012: delete migration plan/progress; fix the Status section that still claimed #1235 unimplemented against its own landing table; demote the 2026-07-10 evidence audit to the end (still cited by the decisions). - 0014: same; the accepted Android blocking-dialog-recovery evidence gap and its covering fixture tests move into Status so the waiver survives. - 0016: verified implemented; rules summary added (nothing was history). No rule's meaning changed; edits are reorganization plus stale-status fixes. * docs(adr): propose ADR 0017 — unified request event journal Apply ADR 0008's registry thesis to events. Inventory (2026-07-24) found four parallel event vocabularies — ~155 stringly-typed diagnostics phases, the session events.ndjson, the progress wire stream, and the replay timing trace (one of its two writers unredacted) — with consumers coupled to emit sites by string: agent-cost counts runner round-trips by matching two phase names. Proposal: an EVENT_CATALOG in contracts making every kind a typed, trait-carrying declaration; the diagnostics scope becomes the single journal append point; every consumer becomes an explicitly registered sink; all existing file/wire formats stay byte-compatible behind golden fixtures. Explicitly rejects pub-sub and event sourcing. Status: Proposed — not indexed in the ADR README until accepted. * docs(adr): revise ADR 0017 per architecture review Address all five review findings and adopt both requested judgments: - P1 out-of-request events: finalizeRepairTeardown records a synthesized close during idle-reap/daemon-shutdown with no live request; a request-scoped-only journal would silently drop it. Added an explicit session-scoped teardown scope model (fatal-scope precedent) and rejected the ambient-fallback alternative. - P1 redaction vs byte-compat: progress stays unredacted on its own channel; the replay-trace unredacted->redacted change is now a declared, intentional compatibility change with its own fixture update, not smuggled under a byte-compat claim. - P1 per-attempt trace routing: sinks with dynamic destinations read scope-bound routing context (logPath-rebind precedent); drop-when-unbound semantics; sink ordering/isolation/flush contract made normative. - P2 progress typing: progress streaming removed from the journal entirely - it is a transport-owned output port (ordering, disconnect-as-cancellation, closed typed union); mirror emits noted as the future opt-in shape. - P2 completeness check: orphan detection is now a static source scan in the layering-lint style; runtime unit-suite observation explicitly rejected. Also per review: catalog keys are internal identities; sinks map to legacy wire discriminators, which are never automatically canonical. Migration plan reduced to 4 steps. * docs(adr): ADR 0017 — fork, never rebind, for per-attempt trace routing Review found a blocking concurrency flaw in the revised routing design: sharded test attempts run concurrently (Promise.allSettled in runReplayTestShards) under one inherited AsyncLocalStorage request scope, so mutable scope rebinding would let one attempt overwrite or clear another's replay-timing destination after an await — cross-writing or dropping events. Replace rebinding with a journal fork primitive: journal.fork(bindings, fn) runs fn in a new ALS scope object sharing the parent's buffer/phaseCounts/ envelope/sinks but carrying frozen routing bindings. Each attempt wraps its work (including nested replay dispatch) in a fork binding its own trace path; the binding dies with the fork, so no clearing step exists to race. Existing updateDiagnosticsScope rebinds stay confined to sequential request setup, pre-fan-out. Validation gains a concurrent-shard regression proving each replay-timing.ndjson contains only its own attempt's events. * docs(adr): ADR 0017 — scope identity on the envelope for future exporter sinks Reserve the one shape decision an OTel-style exporter would otherwise force a retrofit for: every scope (request, teardown, fork) carries scopeId, forks record parentScopeId, both ride the event envelope. Forks already form a tree, so an exporter sink can emit parent-child spans from envelope fields alone. Cross-process correlation stays requestId; a traceparent-style meta field is additive under ADR 0006 and deferred. No exporter in this ADR. * docs(adr): address re-review — renumber to 0018, full fork isolation, 0016 record-as 1. Renumber the proposal 0017 -> 0018: main now carries accepted ADR 0017 (parameterized recorded inputs, #1369); branch rebased onto it. 2. Fork contract strengthened: forks clone EVERY mutable scope field (envelope, logPath, routing bindings) and own their event buffer; only the sink list and the request-global phaseCounts tally are shared. Verified in code: nested dispatch creates child execution scopes (request-router.ts:257) whose updateDiagnosticsScope rebinds session/logPath mid-flight, so a shared mutable envelope would cross-route debug/session-log events between concurrent shards even with frozen trace bindings. updateDiagnosticsScope now specified as mutating only the innermost scope. The regression now covers all three routed outputs (replay-timing, per-request diagnostics ndjson, events.ndjson). 3. 0016 rules summary updated for shipped #1348: sensitive fills use fill --record-as <VAR> (ADR 0017); unparameterized fill/type stays literal (body sections already updated by #1369's merge). * docs(adr): ADR 0018 — name the usage sink as first consumer, privacy by construction The motivating consumer is opt-in usage analytics over agent behavior: command frequencies, typed failure codes, and outcome sequences that trip agents (consecutive snapshots, screenshot-after-snapshot). Decision 4 pins its discipline now, before any exporter exists: an allowlist-by-construction UsageRecord schema whose every field draws from a registry-enumerated vocabulary (command names, ADR 0010 error codes, flag names, durations, hashed session + sequence number) — positionals, selectors, labels, fill text, and error messages are unrepresentable by type, not redacted. Anti-pattern detection is downstream analysis over the stream, never emission-side logic; the sink itself is a follow-up after migration step 3. Adds the matching invariant and a schema gate to validation. |
||
|
|
22a3c4711c |
refactor(daemon): remove the superseded coarse snapshotRefsStale marker (ADR 0014 step 8) (#1268)
* refactor(daemon): remove the superseded coarse snapshotRefsStale marker (ADR 0014 step 8) The coarse `snapshotRefsStale` client-stale marker is fully superseded by the ref-frame model and is removed: - `setSessionSnapshot` and `buildNextSnapshotSession` no longer set/clear it — replacing the latest observation is a read that never touches the frame. - Read-only ref staleness now derives from frame state: a plain ref warns once the frame has EXPIRED (a device side effect changed the screen), and a read-only capture no longer marks refs stale because it does not expire the frame. Pinned-ref warnings keep comparing against the frozen frame epoch. - Deletes `markSessionSnapshotRefsIssued` (its only job was clearing the marker) and the `session.snapshotRefsStale` field. Migrates every test off the marker to the frame model (frame-expiry drives the read warning; complete/partial activation drives admission), and updates the ADR status + module docs to record step 8 as landed. Ships as follow-up to the merged #1257 since that PR closed before this step. Full unit-core + provider-integration green; tsc/lint/fallow/production-exports clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix(daemon): resolve @ref reads from the frame tree; scope find's internal warning Address three review blockers on the coarse-marker removal (ADR 0014 step 8): 1. @ref reads now bind against the authorized frame tree (`refFrameSnapshot ?? snapshot`) in `requireSnapshotSession`, so an internal read-only capture that replaced the observation cannot let a plain `@eN` resolve a different element by positional coincidence. Missing frame evidence fails instead of falling through to a newer observation. 2. A mutating find's internal leaf dispatch (`internal.findResolvedTarget`) no longer attaches a stale-ref warning in either the press or fill path — the caller never consumed a `@ref`, so the public find response must not claim it did. 3. `resolveRefStalenessWarning` checks frame expiry FIRST, matching the admission order: an expired frame is stale for any ref, even a pin that matches the epoch (a matching pin proves identity within the retained frame, not that the UI is current). Regressions: divergent observation-vs-frame trees resolve from the frame tree or fail when evidence is missing; a locator-based mutating find from an expired frame carries no stale-ref warning; the reordered resolver unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix: correct stale-ref warning comments and ADR-0014 present-tense marker refs The get/wait dispatch comments in selector-runtime.ts still described the superseded coarse snapshotRefsStale marker ("warn when that tree was replaced since the client last received refs") even though staleness is now derived from ref-frame expiry (ADR 0014 migration step 8). Reworded both to describe the frame-derived mechanism actually implemented by resolveRefStalenessWarning. session-snapshot.ts's early-return comment in markSessionPartialRefsIssued referenced "the coarse marker" as something still left untouched, but that field no longer exists — reworded to name the ref frame fields it actually preserves. ADR-0014's "Ref frames are separate from operational observations" section still described snapshotRefsStale as part of "the existing... implementation" in present tense, contradicting the Decision section's own note (line 39) that migration step 8 already removed it. Reworded to keep the historical mention while stating the removal. * fix: frame-lifetime wording for the stale-ref warning and read comments Address the follow-up review blocker plus the co-located terminology cleanup (ADR 0014 step 8): - STALE_SNAPSHOT_REFS_WARNING no longer claims "the session snapshot changed"; it now describes frame lifetime in terms valid for both read warnings and mutation rejection — the UI may have changed since the refs were issued, so take a new snapshot before relying on or interacting with them. The warning fires on frame expiry, including device side effects where no stored snapshot changed. - selector-runtime.ts: the get/wait @ref comments now say the read binds to the retained ref-frame evidence and its staleness is frame-derived, not a property of the stored snapshot or the live polling capture. - settle.ts: an unsettled stored capture replaces the observation without touching the ref frame; read staleness is driven by side-effect-seam expiry, not by storing a fresh observation. - interaction-settle.test.ts: renamed the settle test off the removed stale-marker language to "activates a partial ref frame" (what it asserts). Comments/test-name/warning-text only — no runtime behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix(daemon): name the ref-frame epoch in the pinned-stale-ref warning The pinned-ref warning is compared against refFrameEpoch(session) — the frozen frame epoch — not the latest observation generation, and after a read-only capture those two diverge. The message still said "the session tree is now sN", which is ambiguous once the observation counter has advanced past the frame epoch. Name the ref-frame epoch instead: Ref @e12 was minted from snapshot s3 but the session's ref frame is now s15 — re-run snapshot -i. Renames the builder param to `currentFrameEpoch` and corrects its doc comment to say the pin is compared against the frame epoch, not the stored tree generation. Regression: `resolveRefStalenessWarning` names the frozen frame epoch, not the bumped observation generation — a read-only `setSessionSnapshot` advances the observation counter (15 -> 16) while the frame epoch stays frozen at 15; a pin at s15 is clean and a pin at s12 names s15, never s16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
54977f3b87 |
feat(daemon): ADR 0014 session ref-frame lifetime — full implementation (#1257)
* feat(daemon): classify ref-frame effect on every daemon command (ADR 0014 step 2) Add the ADR 0014 `refFrameEffect` trait to the daemon command descriptor facet: every command that reaches a session-owning daemon leaf declares how it relates to the session's authorized ref frame — `preserve`, `may-invalidate`, `delegated`, or a request-sensitive resolver for subaction-dependent commands (keyboard status vs dismiss, alert get/wait vs accept/dismiss). This is the honesty/completeness guard, not the transition site: a `may-invalidate` command still calls the (future) ref-frame module only when its mutating path runs. No runtime behavior changes here. - `RefFrameEffect` / `DaemonRefFrameEffect` types and a `resolveRefFrameEffect` accessor honoring the resolver form, mirroring the existing closure traits. - Classify all 58 daemon-faceted commands; `find` is the honest superset (`may-invalidate`) pending a read/mutate resolver during enforcement wiring. - Give `app-switcher` a daemon facet (route unchanged) so the generic-fallback escape hatch the ADR calls out is covered instead of silently unclassified; drop it from parity's UNROUTED set. - Completeness gate (`ref-frame-effect.test.ts`): every daemon-projected command classifies an effect, every public command is classified or in the explicit non-daemon allowlist (`install-from-source`, which projects via the `install_source` internal command), and the resolvers/app-switcher resolve as declared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): introduce ref-frame module + admission matrix (ADR 0014 step 1) Introduce `src/daemon/ref-frame.ts` as the single owner of the ADR 0014 ref-frame model — the authorization namespace for mutation refs, kept distinct from the latest operational observation (`session.snapshot`). It defines the frame's issuance scope and lifecycle state and the pure mutation-admission matrix (`admitRefMutation`) with the ADR's typed, order-sensitive reasons: ref_frame_expired, ref_generation_mismatch, plain_ref_requires_complete_frame, ref_not_issued. The frame is introduced behind the existing `snapshotGeneration` (epoch) and `snapshotRefsStale` (coarse client-stale) fields, whose wire-visible names (`refsGeneration`, the `@e12~s42` pin grammar) are unchanged. New `refFrameState`/`refFrameScope` session fields default to active/all, so the matrix currently reduces to the generation-pin check the iOS path already did — no behavior change. Expiration at the side-effect seam and non-`all` scope land in later steps. The existing #1241 iOS stale-ref guard now routes its decision through `admitRefMutation` (plus the transitional coarse-stale check for plain refs), so the module is production-live; the external error contract is identical. Adds a unit test covering the full admission matrix and reason ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): wire pre-side-effect frame expiration at the seams (ADR 0014 step 3) Route device mutations through the idempotent ref-frame transition. A leaf expires the current frame synchronously, immediately before awaiting the device operation, so success, timeout, cancellation, or connection loss all leave it expired — there is no success-only rollback. Seams wired: - interaction runtime backend closures (tap/click, fill, longPress, native web clickRef/fillRef, gesture, type) — post-resolution, pre-dispatch, so a resolution failure before the seam preserves the frame; - the generic daemon leaf (back/home/rotate/scroll/tv-remote/app-switcher/ viewport/focus, ...), gated by the daemon `refFrameEffect` classification via `resolveRefFrameEffect`, which is that resolver's first production consumer. Re-authorization: issuing a complete namespace re-activates the frame — `markSessionSnapshotRefsIssued` and the snapshot command's `buildNextSnapshotSession` — so a fresh capture between mutations restores usability. A diff or kept tree preserves the prior authorization state; internal read captures never re-authorize. Enforcement of the new expired-frame rejection is intentionally deferred to step 7, which the ADR gates on fresh live device evidence per platform. The iOS #1239 guard therefore stays armed-but-not-enforced here: it consults the admission matrix but still rejects only on the pre-existing conditions (pinned generation mismatch, coarse plain-ref stale marker), so behavior is unchanged. Tests prove the transition is wired (a press expires the frame; a re-issue re-activates it) alongside the idempotency and re-authorization unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix(daemon): address ADR 0014 review — partial issuance, keyboard, seam coverage Exact-head review found three blockers; all fixed with focused seam tests. 1. Partial issuance no longer restores complete authority. Every caller of `markSessionSnapshotRefsIssued` (find, settled diff, replay divergence) is a PARTIAL publication, but it re-activated a complete `all`-scope frame. It now only clears the coarse marker; complete re-authorization is reserved for the snapshot command (`activateCompleteRefFrame`, from `buildNextSnapshotSession`). 2. Keyboard resolver covers every mutating subaction. keyboard accepts status/get/dismiss/enter/return; only status/get read, so dismiss/enter/return (enter/return dispatch a real return key) are now `may-invalidate`. Alert reads are likewise a named set. Completeness test extended. 3. Remaining step-3 leaf seams wired: the direct iOS selector fused dispatch, the direct `find` focus/type dispatches (find click/fill already delegate through the interaction leaf), and Android blocking-dialog recovery (expire before the recovery tap). Focused seam tests for each prove the frame expires. Enforcement of the expired-frame rejection remains deferred to step 7 behind the ADR's per-platform live-evidence gate; behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): cross the seam at every specialized mutating leaf (ADR 0014 step 3 complete) Wire expireRefFrame at the remaining may-invalidate leaves so EVERY mutating daemon leaf crosses the side-effect transition, not just the interaction/generic paths: - keyboard dismiss/enter/return, push, trigger-app-event (shared session leaf) — gated by resolveRefFrameEffect so keyboard status/get preserve the frame; - alert accept/dismiss (get/wait preserve, via the alert resolver); - settings mutations; - React Native overlay dismissal; - install / reinstall (deploy op); - open / relaunch — expires the reused session's frame before the launch; - close — expires for uniformity, though a successful close deletes the whole session (and its frame) anyway. Seam tests: keyboard dismiss expires while status preserves (proves the resolver-gated pattern), and RN overlay dismissal expires. Enforcement stays deferred; behavior unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): partial issuance scope + MCP pin retention + pinned CLI refs (ADR 0014 step 4) A find/settled-diff/divergence result publishes only the refs it returned, so it now activates a bounded PARTIAL frame authorizing exactly those ref bodies (`markSessionPartialRefsIssued`) instead of nothing — a plain ref then requires a complete frame and a pinned ref outside the set is rejected. An empty partial result leaves prior authority intact. - read-only find publishes its one ref; settled diff publishes its added lines + `refs` + `tail`; divergence publishes its capped, non-covered, non-chrome digest set. - MCP: a mutating `find` returns no `refsGeneration` and is explicitly non-issuing — it no longer hits the missing-generation branch that wiped the whole per-session pin scope (forwarding the old pin is how the daemon produces a precise stale rejection). - Human-CLI partial results render reusable refs in ready-to-copy `@eN~s<gen>` form (find + settled tail); JSON/Node keep plain bodies + one response-level generation, and MCP stays plain (it auto-pins). Output-economy waiver covers the +8-byte tail-pin increase with an ADR justification; the workflow oracle treats a pinned ref as surfacing its plain body. Enforcement of the frame's expiry and partial-scope rejections stays deferred to step 7 (behind the ADR's per-platform live-evidence gate), so this is behavior-preserving; the iOS guard now consumes the admission verdict for a typed `details.reason` on the rejections it already emitted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): resolve refs against the authorized frame tree (ADR 0014 step 5) Retain the ref frame's immutable source tree (shared reference, no deep copy) and resolve a `@ref` against it rather than the latest operational observation. An Android freshness — or any read-only — capture advances `session.snapshot` without disturbing the frame tree, so the two intentionally diverge. At resolution, adopt the fresh observation's node (its current on-screen coordinates) ONLY when its local identity still matches the authorized node — the legitimate "element moved" case. If a different element now sits at that index, keep the authorized frame node so a positional coincidence cannot retarget the action. Expose the frame tree to the command runtime through `CommandSessionRecord.refFrameSnapshot`; pre-frame sessions fall back to `snapshot` and behave exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * feat(daemon): fail-closed ref-mutation enforcement across platforms (ADR 0014 step 7) Enforce the ref-frame admission matrix on every platform before dispatch: an expired frame, a superseded generation pin, a plain ref against a partial frame, or an unissued pinned ref is now rejected with a typed `details.reason` and an honest message that names the lifetime failure instead of claiming the ref was missing or lacked bounds. The prior iOS-only, coarse-marker guard is replaced. Freeze the frame epoch at issuance (`refFrameGeneration`) so a later read-only capture that advances the observation counter cannot falsely reject a correct pin from the issuing frame; staleness warnings compare against the same frame epoch. A mutating `find` re-resolves its target by locator against a fresh capture, so its internal leaf dispatch carries `internal.findResolvedTarget` and skips ref admission (it still crosses the seam and expires the frame). Update unit and provider-integration scenarios to the new contract: multi-mutation ref sequences re-observe between mutations, settled refs are consumed in pinned form, and rejections assert the typed reason. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * docs(adr-0014): promote ref-frame vocabulary and mark implementation status Flip ADR 0014 to Accepted, promote the ref-frame / frame-expiry-seam / mutation-admission vocabulary into CONTEXT.md, correct the `@ref` resolution note to the frame-tree model, record the migration status (steps 1–7 landed; coarse-marker removal follows live-evidence confirmation), update ADR 0012's divergence-ref amendment to accepted, and add a CHANGELOG entry for the fail-closed ref lifetime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * test(daemon): lock ADR 0014 evidence #1 and refresh module docs Add a daemon-level sequence test proving the canonical contract: after an unobserved first ref mutation, a second mutation rejects both bare and pinned with ref_frame_expired, and a fresh snapshot re-authorizes. Refresh the ref-frame module header and seam-expiry test comment now that enforcement is live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix(daemon): address ADR 0014 exact-head review — six lifetime blockers 1. Android dialog recovery aborts an outstanding ref action: a ref press/fill admitted against the pre-recovery frame now fails with ref_frame_expired when before-command recovery mutates the UI, instead of continuing against the recovered screen (selector/coordinate actions still re-resolve and continue). 2. open --relaunch expires the existing session's frame BEFORE the close dispatch, so a close timeout/failure that already tore the app down still leaves the old frame expired. 3. expireRefFrame clears scoped-snapshot lineage (snapshotScopeSource) at the seam, so snapshot -s @ref -> mutation -> snapshot -s @same-ref can no longer borrow stale lineage across a device side effect. 4. Missing authorized-frame evidence fails closed: resolveSnapshotForRef no longer recaptures and accepts the same ref body from a newer tree by positional coincidence. A mutating find's internal dispatch resolves against its own fresh capture (omitRefFrameSnapshot), not the frame. 5. Mutating find omits refsGeneration — its acted ref is diagnostic pre-action identity and must not be pinnable after the action. 6. An empty partial publication leaves all session state untouched (including the coarse marker), instead of clearing it before finding there were no refs to issue. Adds focused regressions (lineage-cleared sequence, empty-partial no-op, fail-closed on unusable bounds, in-frame label recovery, mutating-find non-issuance) and extracts the find action dispatch to keep complexity in budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * fix: preserve snapshot refsGeneration + shared recovery rejection (ADR 0014 re-review) P1: structured JSON/Node snapshot results now retain the response-level refsGeneration. It was declared on the daemon response but dropped by the public CaptureSnapshotResult type, the serializer, and the Node normalizer, so default `snapshot -i --json` emitted refs with no generation to pin against. Added to the type, serializer, normalizer, plus CLI/Node tests. P2: Android dialog-recovery abort now reuses the SHARED admission rejection (refMutationAdmissionResponse) instead of a bespoke error, so the failure carries the full typed context (reason, ref, currentGeneration, scope, mintedGeneration) identical to every other expired-frame rejection across platforms. Removes the now-unused AppError/refFrameState imports. Adds a regression proving recovery aborts the outstanding ref action before any press dispatch. Also adds the relaunch failure-boundary regression (existing-session close fails after dispatch → old frame stays expired), and corrects the ADR implementation-status note so Android blocking-dialog recovery and a real provider-backed interaction/lifecycle are recorded as unexercised release blockers rather than confirmed enablement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * docs(adr-0014): record provider seam as live-verified; Android recovery sole blocker The provider-backed interaction + lifecycle seam is now confirmed by fresh live evidence (AWS Device Farm, webdriver backend). Update the ADR implementation-status note so only Android blocking-dialog recovery remains an unexercised release blocker — and note it is blocked on a bootable free Android target plus a deterministic app-owned ANR trigger, not on any code gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa * docs(adr-0014): record Android ANR recovery as an accepted evidence gap Per the review decision: the Android blocking-dialog recovery seam has no deterministic app-owned ANR repro in the harness, so it was not live- exercised. The team accepted shipping without a live run for it — its transition/abort logic is covered by fixture regressions and it is enforced in code identically to the verified paths. Reword the status note from an open release blocker to a documented, accepted evidence gap, which unblocks step 8's coarse-marker removal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
392dc1cded |
refactor: rename rotate command to orientation (rotate kept as deprecated alias) (#1252)
* refactor: rename rotate command to orientation, keep rotate as a deprecated alias
The top-level `rotate` command (device orientation: portrait/landscape) shared
a name with the `gesture rotate` two-finger rotation gesture. Rename the
orientation command to `orientation` and keep `rotate` working as a minimal,
silent CLI alias (same mechanism as `tap`->`press`) for a few versions.
The rename is applied across every layer:
- command-descriptor registry `name`, daemon dispatch handler, and the typed
system facet (metadata/cliReader/daemonWriter/schema/output formatter)
- navigation projection + `CommandResultMap` (`OrientationCommandResult`,
`action: 'orientation'`), client types (`OrientationCommandOptions`), and the
runtime family (`device.system.orientation`)
- interactor + backend methods -> `setOrientation` (matching the backend's
`setKeyboard`/`setClipboard` verb convention); Android helper
`rotateAndroid` -> `setAndroidOrientation`
- Apple/cloud-webdriver capability keys and plugin gate
- user-facing docs (commands.md, client-api.md)
Client SDK method is `orientation` (client convention = camelCase of the
command name, matching `back`/`home`/`appSwitcher`); execution layers use the
imperative `setOrientation`.
Deliberately unchanged:
- the Swift runner wire protocol keeps `command: 'rotate'` — the runner has its
own command namespace with no gesture collision, so renaming it would only
risk CLI<->installed-runner version skew on physical devices
- the `DeviceRotation` value type / `parseDeviceRotation` (names the orientation
values, no collision)
Note: `client.command.rotate` / `device.system.rotate` and the `RotateCommand*`
exported types are removed (the alias only rewrites CLI tokens); SDK consumers
must use `orientation`. The JSON `action` value changes `rotate` -> `orientation`.
* style: wrap long lines to satisfy oxfmt (orientation rename tests)
* fix: add compatibility layer for the rotate->orientation rename
Addresses review blockers on the CLI-only alias: `rotate` previously
resolved only in CLI token parsing, so command-data/RPC paths that carry
the wire command directly failed descriptor validation, and the removed
typed SDK surface broke shipped consumers.
Central command-alias boundary (was CLI-only):
- Promote `cli-command-aliases.ts` to `command-aliases.ts` as the single
alias source, applied at each command-name ingress that bypasses the CLI
parser: the daemon request boundary (`handleRequest`, covering replay and
older remote clients) and the batch step readers (CLI `batch-steps.ts` and
daemon `batch-policy.ts`). No hand-synced command tables.
Retain deprecated typed SDK surface (shipped v0.18/v0.19):
- `RotateCommandOptions` / `RotateCommandResult` type aliases (legacy
`action: 'rotate'` contract) and `SystemRotate*` runtime types.
- `client.command.rotate` and `device.system.rotate` deprecated wrappers
that delegate to `orientation` and restore the legacy response
(`action: 'rotate'` / `kind: 'systemRotated'`).
ADR 0014: rename `rotate` -> `orientation` in the invalidation guidance
(lines 229, 237) so the accepted architecture doc matches the command name.
Tests: daemon-boundary rewrite, CLI+daemon batch alias resolution, and the
deprecated client/runtime wrappers preserving the legacy contract.
Live emulator evidence (emulator-5554):
- `orientation landscape-left` -> user_rotation=1
- `rotate portrait` (CLI alias) -> user_rotation=0
- batch step `{command:'rotate'}` (no CLI parser) -> user_rotation=1
* fix: preserve orientation rename compatibility
* test: stabilize orientation compatibility formatting
* style: format MCP compatibility test
* revert: drop cross-surface rotate compatibility, keep the lean rename
The rotate->orientation change is a bug fix (name collision with the
`gesture rotate` two-finger gesture), not a compatibility feature. The
cross-surface command-data compatibility added disproportionate weight
(~480 B, dominated by the alias module inlined into the batch bundle) for a
command that was only canonical for two minor versions, so shipped batch/
replay/MCP data carrying `rotate` is a rare, documentable break.
Removed:
- daemon request-boundary command normalization (`request-router.ts`)
- batch step alias resolution (`batch-policy.ts`, `cli/batch-steps.ts`)
- MCP tool-runner alias/legacy-result handling (`mcp/command-tools.ts`)
- the `command-aliases.ts` module rename and cross-surface machinery
(reverted to `cli-command-aliases.ts`)
- the cross-surface tests
Kept (cheap, high value — prevents build breaks for typed consumers):
- CLI `rotate` alias (one line, same mechanism as `tap`/`launch`)
- deprecated `RotateCommand*` / `SystemRotate*` type aliases and the
`client.command.rotate` / `device.system.rotate` wrappers that delegate to
`orientation` and restore the legacy response contract
Net bundle vs main is now +473 B (was +952 B), almost all the kept SDK
wrappers plus the unavoidable longer command name.
|
||
|
|
6c416385ca |
docs: define session ref-frame lifetime (#1247)
* docs: define session ref-frame lifetime * docs: address ref-frame ADR review * docs: resolve ref-frame contract blockers |