refactor: migrate get to the request-bound device runtime - #1877
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head
The branch’s own reconciliation and size blockers also remain: it must reconcile with |
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
Both code blockers resolvedNew head: Blocker 1 — direct-iOS selector path completed before admissionFixed by ordering, not by reverting the descriptor. To be explicit about what this does and does not claim: Red before green. New regression The fast path answered Blocker 2 — untyped fallback
| Readonly<{ status: 'read'; text: string }>
| Readonly<{ status: 'unreadable'; reason: ElementTextUnreadableReason }>with
This is the generic-catch retirement my start record promised, so it is a deletion I already owed. Red before green. New regression Planted violation for the exhaustiveness gate. Adding a third reason to the single source of Both the compiler and the contract test go red; both are green with the planting removed. Contract coverage: On the ADR 0011 path classificationI did not add a row to Behavior change, declaredAn unexpected live-read failure (runner transport, helper crash) during Validation
One correction worth recording, because it cost a cycle and generalizes. An earlier attempt That failure was itself a real finding: only I resolved it by satisfying neither: the runtime reason list is deleted outright. Exhaustiveness The planted Live re-verificationRe-run on the assigned iOS Simulator
Still open, per the coordinator's sequencingThe The scratch-dir wart is fixed — it now lives outside the worktree, so |
|
Re-reviewed exact head |
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
c666150 to
4778a27
Compare
The selector capture seam lands here, with
|
Live re-verification of the shipped seam pathThe seam swap changed both how
Session closed; no sessions left open. |
|
Blocking: #1739’s batch decision requires the shared read path to retire jointly, even while |
|
Out of draft at Stack: #1875 → #1877 → #1876 → Both blockers from your review at
This PR also carries the selector capture seam, because Three things worth the hardest look:
Live-verified at this exact head on |
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
4778a27 to
8aed44f
Compare
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
8aed44f to
52a1c9d
Compare
|
Blocking on exact head |
|
That review is against Verified on the pushed head:
Per the tracker ruling, the shortcut is retired in Two helpers went unreachable with the caller and are deleted: On the R36's A cost stated rather than buried: this is a real latency regression. The body refresh you asked for is in progress along with the CI Size figure and |
|
Re-reviewed exact head Not ready yet: the branch conflicts with current |
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
aa12c9f to
9e59a55
Compare
Live verification at
|
| before (pre-retirement head) | now (9e59a55bd) |
|
|---|---|---|
querySelector runner commands |
0 → 3 | 0 → 0 |
| answer | General |
General |
The command still answers correctly and issues zero querySelector runner commands — it now
resolves through the bound capture like every other get shape. That is the on-device counterpart
of the unit test that replaced the one asserting the old behavior.
Every path, including the retired path's failure case:
get text @ref→General;get attrs @ref→ correct nodeget text label=…→Accessibilityget text label=NoSuchThing→COMMAND_FAILED: Selector did not matchget text id=no.such.identifier→COMMAND_FAILED: Selector did not match— the not-found
case of the retired path, now answered by the snapshot pipeline rather than the runner queryget text @e999→COMMAND_FAILED: Ref @e999 not found
The two retired chains, still absent:
querySelectorrunner commands: 0- legacy
readdispatches (platform_command_preparewithcommand: "read"): 0
The §9 measurement holds unchanged through the Interactor seam — the preferred read now
resolves via localInteractors.resolve rather than the deleted host port, and recovers the same
text:
- required path alone (captured tree,
get attrsvalue): 511 chars on a[truncated]node - bound preferred
readTextAtPoint: 1,126 chars, via 3readTextrunner calls
So the mechanism change is behaviour-neutral on device, and the latency cost of the retirement is
confined to what was declared: get text id=… now captures the tree instead of skipping it.
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
9e59a55 to
865145e
Compare
|
Re-reviewed exact head One contract blocker remains: Size is accepted by the recorded #1739 ruling now that the duplicated read path is retired; refresh the body with that ruling and the exact-head CI deltas (+3.5 kB raw, +1.2 kB gzip, +1.0 kB tarball, +4.0 kB unpacked). No |
`get` declares `elementReadRuntimeUse` (required `captureSnapshot`, preferred `readTextAtPoint`), admits once from exact owner facts, refuses before binding, and binds exactly once. Its capability bucket, the static HarmonyOS/Web command sets that augmented it, and `requireCommandSupported` admission for `get` are gone; `'get'` leaves the `createSelectorRuntime` capability union. The neutral `readTextAtPoint` operation replaces the branch-per-family legacy `read` dispatch on the `get` path. Every local family and both providers now classify it exhaustively — Web, HarmonyOS, Vega and every provider row report it unavailable, which is behaviour-preserving because the legacy dispatch had no arm for them and threw on every call before falling back. R36 is the new parametrized cutover row.
…ad outcome Review blockers on #1877. 1. `dispatchGetViaRuntime` could complete the direct-iOS selector query before `resolveBoundGetRuntime`. Once `get` declares `device-runtime`, ADR 0019 requires resolve -> admit -> bind before anything in the request path operates, so admission now runs first for every target shape and the fast path is a fast path *within* an admitted request. Regression: an eligible direct selector cannot operate when facts refuse admission. 2. `readTextAtPoint` returned `Promise<string>` and `readTextForNode` caught any throw and fell back, assigning a typed diagnostic after an untyped failure. It now returns a closed `ElementTextReadOutcome`; fallback happens only for the contract's classified reasons; unexpected errors propagate. The reason union is derived from its runtime list so the two cannot drift, and an unhandled reason is a compile error at the consumer. This retires the generic catch the start record promised.
…nsumer Takes ownership of the request-bound selector capture seam from #1876, which cannot ship standalone: with find's cutover deferred it had no consuming command (ADR 0019 §10) and was not dead-code clean (check:production-exports 19 -> 20). `get` is its first consumer, so it lands here. Adopts find's handoff as given. The one shape change, approved by the coordinator: the selector family gets its own capture uses carrying a PREFERRED `readTextAtPoint`, declared ALONGSIDE the snapshot uses so `snapshot`/`diff` keep binding exactly what they bind today. The read is surfaced through the existing arms of `bindSnapshotCaptureRuntime`, reusing the same selectActiveAppSnapshot / selectSnapshotWithoutActiveApp selectors — no second plan-to-operation dispatch. `get` now runs through `createBoundSelectorRuntime`; `resolveBoundGetRuntime` and its test are deleted as superseded, and `'get'` leaves the `createSelectorRuntime` capability union. The legacy read adapter survives for `find <q> get text` and is selected by which command constructed the runtime — never by failure, family, environment, or flag — so `get` cannot reach it. It retires in find's cutover, where the last consumer moves.
Read-only `find` now constructs a BOUND selector backend, so `get text` and
`find <q> get text` execute the same bound `readTextAtPoint` instead of one
binding it and the other dispatching the legacy `read`. This moves find's READ
LEG only: find's descriptor stays LEGACY_PLATFORM_EXECUTION and it claims no
cutover row.
With no consumer left, the whole chain goes: the `read` registry entry and its
`dispatch: {}` projection, `DISPATCH_HANDLERS.read`, `handleReadCommand`,
`interaction-read-legacy-dispatch.ts`, and the duplicate platform reader
branches it carried. `read` was the only `dispatch-alias` descriptor, so that
catalog group goes too.
Deleting the registry entry drops 'read' from DescriptorDispatchCommandName,
which makes a surviving DISPATCH_HANDLERS.read a compile error rather than
something R36 has to police. R36 now claims the retirement it can prove.
`find.test.ts` is over the size tripwire, so its handler invocation is
extracted to find-handler-fixture.ts and the pin lowered 1237 -> 1221.
Two edits, per find's ADDENDUM.md: 1. `includeRects` returns to `buildRuntimeCaptureInput`. It was removed from #1876 as unconsumed; the selector capture path is genuinely its first consumer (a Web rect capture requests bounds explicitly), so it lands here under the same rule that moved the seam. `snapshot`/`diff` pass nothing. 2. The per-capture `signal` is dropped, not restored. `CaptureSnapshotInput` has no such field on this stack — it moved to `wait` (#1875) with the regression that proves per-poll abort and quiescence. `get` captures once per resolution and never polls, so nothing here needs it. The seam test and fixture coverage for it moves with the contract rather than being kept against a field that no longer exists.
`get` declares device-runtime, so its request path must reach the platform only through operations R36 declares. `dispatchDirectIosSelectorGet` reached `runAppleRunnerCommand` through a path the row declares no operation for; admitting before a bypass is not executing through the seam, so the bypass is removed rather than ordered after admission. Every target shape — including the simple iOS `id=` selector — now resolves through the bound capture. `queryDirectIosSelector` itself stays: `offscreen-target-probe.ts` still consumes it and it remains single-copy. `dispatchDirectIosSelectorIs` belongs to `is` (#1883). Two get-only helpers (`readDirectIosGetSelector`, `buildDirectIosGetResult`) became unreachable and are deleted with the caller. Declaring `querySelector` as a fact-admitted preferred operation was rejected on duplication, not correctness: the offscreen probe takes a plain session and cannot consume a bound operation, so it would ship the query twice until Wave 5 moves the probe — the deferred-duplication shape this PR was already overruled for on the `read` alias. It returns as a declared, §9-measured operation in a later unit that also moves the probe. Cost, stated plainly: `get text id=…` loses its tree-capture skip on iOS. No fallback was added and the latency is not recovered elsewhere. R36's singularExecution claim is now what the code does rather than aspirational.
…poke host Two operations of the same class were reaching their mechanics two different ways: `findText` rides `Interactor` via `localInteractors.resolve`, while `readTextAtPoint` had its own host port. That is duplication of MECHANISM, so the read now rides the same seam. `Interactor` gains `readTextAtPoint?`, implemented on the Apple, Android and Linux interactors where those mechanics already live. `src/platform-runtime-element-text-host.ts` and its `elementText` host wiring are deleted; the contract binds through the resolver exactly as the snapshot runtime does. Size honesty: this removes an 89-line module but the four readers still have to exist, so they moved into the interactors rather than vanishing. Net production change is ~4 lines, not ~89. The duplication of mechanism is what is actually fixed; Wave 5/6 retires the seam for both operations together. Also from the size investigation: - `ElementTextRuntimeExecution` was byte-identical to `SnapshotRuntimeExecution`; removed and reused, as `find-text-runtime.ts` does. - Removed a stranded, stale comment in `selector-capture-binding.ts` that still claimed a duplication this branch had already retired. - `FrozenUnavailablePlatformRuntimeFacts` is derived from its input type rather than restated, removing a 14-line clone group my new cell had pushed over the detector threshold.
* refactor: migrate is to the request-bound device runtime `is` declares the shared selector capture use, admits once from exact owner facts, refuses before binding, and binds exactly once. Its capability bucket, the static HarmonyOS/Web command sets that augmented it, and `requireCommandSupported` admission for `is` are gone; `'is'` leaves the `createSelectorRuntime` capability union. Admission now runs BEFORE the direct-iOS selector fast path. ADR 0019 requires resolve -> admit -> bind before anything in a `device-runtime` command's request path reaches the device, so that query becomes a fast path *within* an admitted request rather than a way around exact-owner facts. The rule is documented once, on `createBoundSelectorRuntime`, replacing the two duplicated call-site comments `get` and `is` were each carrying. Declared behaviour change: `is` takes the active-app plan split, so the facts decide per family. On iOS `appBundleId` is the XCUITest attach target — with no tracked app the runner's own process comes to the foreground, displaces the app under test, and the capture then answers confidently about the runner's own blank screen. An iOS `is` on a session with no tracked app is now a typed SESSION_NOT_FOUND refusal carrying the `open` hint. Refusing beats displacing-and-lying. Android captures the real launcher in that state and is unchanged, which is what the platform facts already encoded. The two Apple watchOS cells move from capability-admitted-then-runner-failure to a typed unavailable refusal, the same classification snapshot, diff, and get already landed. R37 is the new parametrized cutover row. `find` keeps `createSelectorRuntime` and its `requireCommandSupported` call, so `captureData` stays optional and `captureSnapshotWithInteractor` stays: this unit is not the last selector unit. * fix(is): a failing iOS assertion fails instead of exiting zero Reverses part of #557, on thymikee's explicit instruction. `is` is an assertion: the docs state it "exits non-zero on failure". The direct-iOS fast path broke that contract — it reported a failed predicate as a completed command, so on device $ agent-device is text id=… "Wrong Expected Text" Passed: is text (exit 0) because `{ok: true, pass: false}` reaches `isCliOutput`, which renders "Passed: is <predicate>" without reading `pass`. A failing assertion reported as success lets a replay run on past a broken state. Now: Error (COMMAND_FAILED): is text failed for selector id=…: expected="Wrong Expected Text" actual="Apple Account, …" (exit 1) The renderer needed no patch: a negative can no longer produce a success envelope, so it is correct by construction. Direction chosen deliberately. Making the two paths agree could have gone either way, and "an agent asked a question and got an answer" is a real argument for the other one. This follows the DOCUMENTED contract rather than merely the incumbent behaviour, and the alternative is a far larger change: a zero-exit `is` would alter every platform and path, break scripts that rely on it failing the shell, and needs its own PR, docs, and probably a major version. It is also already how `is hidden` and `is exists` behave end to end. PASSING assertion, and that arm still answers with zero captures (pinned). Only the negative falls through — what #557's own summary asked for, "preserving snapshot fallback for misses", refusing fallback only for hard failures like ambiguity. The fall-through was #557's own design, never armed: the `| null` return and the caller's `if (!payload) return null;` guard were unreachable. This makes that dead guard live. Measured on iPhone 17 (median of 9, warm daemon): predicate holds 0.14s / 0 snapshots, unchanged; predicate fails 0.25s / 1 snapshot. ~+0.11s on failing assertions only. Correctness gain beyond the envelope: the fast path evaluates a ONE-NODE tree, so `visible` cannot see the ancestor geometry a list row inherits and its negative can be wrong. Falling through re-asks the real tree and can turn a spurious negative into a pass. The #557 pin moved with its reasoning at the pin site. * fix(layering): let a cutover row state a data-only admission retirement Review blocker on #1883: R37 claimed `legacyRetirement.routeNames: ['WEB_QUERY_COMMANDS_WITH_IS', 'HARMONYOS_IS_SUPPORT']`. Neither identifier has ever existed. They satisfied the non-empty shape check while proving nothing — the vacuous registry claim AGENTS.md warns about, and a green gate that would stay green if the deletion were reverted. The cause was the model, not the row. Every `LegacyRetirementClaim` form names something that must NOT exist, which a row can always satisfy by inventing a name. `is` retired no module, route, or dispatch projection because it had none: its legacy admission was a capability bucket plus membership in two static platform command sets, so its real retirement is a DATA deletion the model could not express. Rather than patch around that with sentinels or a per-command policy file — both forbidden by the playbook — this generalizes the model. `staticCommandSets` names the sets themselves and is proven from both sides: each must still be DECLARED in production source, and must no longer list the command. A fictional set fails the first half; a skipped deletion fails the second. That is what an identifier-shaped claim cannot state. R37 now claims HARMONYOS_SUPPORTED_COMMANDS and WEB_QUERY_COMMANDS, which is the deletion it actually performed. Planted red, both halves, against the real gate: [R37 is-runtime-cutover] 2 violation(s): (is cutover row):1 — claims retired static command set 'WEB_QUERY_COMMANDS_WITH_IS', which no production source declares (is cutover row):1 — claims retired static command set 'HARMONYOS_IS_SUPPORT', which no production source declares [R37 is-runtime-cutover] 2 violation(s): src/core/capabilities.ts:59 — static command set WEB_QUERY_COMMANDS still admits is so the exact claim that shipped is now rejected by name, and so is restoring the membership it claims to have removed. Mechanism cases live with the other planted-row tests; layering goes 177 -> 181. * test(is): pin the exit-code guarantee independently of what answers the predicate Prep for the Blocker 1 retirement, which deletes `buildDirectIosIsResult` — the function the #557 reversal fixed. The reversal's guarantee must not evaporate with it, so it gets a case that does not know how the daemon decided. `is` is documented to "exit non-zero on failure". The reversal proved that at the JSON envelope; nothing pinned it at the CLI boundary, which is where the defect was actually visible (`Passed: is text`, exit 0). This asserts the CLI contract directly: a `predicate_failed` response exits 1 and never renders as passed. It survives the retirement untouched, because it asserts the outcome rather than the path. Planted red with the exact pre-#1739 envelope the shortcut produced (`{ok: true, data: {pass: false}}`): `exitSpy.calls` is `[]` — no exit call at all — so the case fails, which is the regression it exists to catch. Unpushed on purpose: the restack will carry it into the retirement cycle. * refactor(is): retire the direct-iOS selector shortcut thymikee's ruling (option b). `is` declares `device-runtime`, so its request path must reach the device only through the operations R37 declares. It did not: a simple iOS `id=`/`label=` target was answered by a direct XCUITest querySelector without any capture, ordered after admission but not executing through the seam. This is not retired because it was wrong. `wait` hypothesized that the degenerate one-node evaluation mis-answers `is visible` for off-viewport nodes, traced it through the code convincingly, then tested it on device and it did not reproduce — XCUITest's own query is conservative about visibility, so the degenerate evaluation never gets the chance. It is retired because it was an undeclared, unmeasured bypass that made R37's singularExecution claim false: the same class of untruth as the sentinel retirement names fixed in the previous commit. Declaring querySelector as a real operation instead was rejected for a concrete reason: offscreen-target-probe.ts consumes queryDirectIosSelector with a plain session and cannot take a bound operation, so declaring it now would ship it twice until Wave 5 moves the probe — the deferred-duplication shape that got get's read deferral overruled. It returns as a declared, fact-admitted, section 9-measured operation in the unit that also moves the probe. Retired: dispatchDirectIosSelectorIs, its call site, buildDirectIosIsResult, and resolveDirectIosSelectorQuery — each had exactly one caller, all on this path — plus the ResolvedDirectIosSelectorQuery type they orphaned and two imports. queryDirectIosSelector itself stays: the offscreen probe still consumes it and it remains single-copy. Latency cost, stated plainly and not softened: a held predicate on a simple iOS selector goes from ~0.14s with no capture to ~0.25s with one, measured as the median of 9 warm runs on iPhone 17. There is no fallback and no fast path. R37's comment finally describes the code: "every predicate answers from the resolved tree" was written while the shortcut existed. Its scope is now stated too, so it is not read as absolute — the Android foreground-blocker diagnostic still reaches adb on the failure path, where it cannot produce or change a verdict; that edge is pre-existing, co-owned with wait, and recorded as Wave 6 denominator work with R22's appState as its declared replacement. Seven tests lost their subject. Those whose only content was the shortcut's own mechanics are deleted; the outcome-level ones are retargeted and keep asserting what survives. --------- Co-authored-by: agent <agent@local>
58a21e9 to
4a1cf85
Compare
… bug
An owner whose facts advertised `readTextAtPoint` but whose interactor cannot
perform it was reported as `{ status: 'unreadable', reason: 'surface-not-readable' }`.
That put a contract violation inside the closed reason set that licenses falling
back to the captured tree, so `get text` answered from potentially stale snapshot
text precisely because the runtime lied about itself. ADR 0019 §2 requires the
mismatch to fail as `runtime-contract-invalid`; it now throws.
Removing the only producer of `surface-not-readable` made that reason dead: no
path can reach it, since an interactor that HAS the read maps a blank or absent
answer to `no-text-at-point` via `elementTextRead`. Dropped from the union, its
consumer switch arm, and both test lists. `classifiedFallbackReason`'s `never`
arm stays — it is what makes adding a reason a compile error rather than a
silent untyped fallback.
Deduplication found while auditing the change:
- `invalidRuntimeContract` was module-private in `platform-runtime.ts`. It now
owns its own module so both runtime modules share one construction. It is
deliberately not exported through the platform facade: that facade must stay
exhaustive over its sources, which would make this a public symbol with no
external consumer.
- The 8-field runner execution projection was written out three times
(`snapshot-runtime-capture-input.ts`, `interaction-read.ts`,
`screenshot-runtime.ts`). One `runtimeExecutionFromContext` now serves all
three; `screenshotExecutionFromContext` keeps its name and delegates, since
`ScreenshotRuntimeExecution` and `SnapshotRuntimeExecution` are the same type.
Dropping a field here silently strips request id, log/trace paths, XCUITest
overrides, or runner lease context — an operation that still answers but runs
unconfigured, which is exactly the defect the wait unit hit as a P1.
Red before green: with the old guard restored the new regression fails with
"Missing expected rejection" — the call resolves instead of throwing, which is
the silent degradation it exists to forbid.
|
Re-reviewed exact head |
|
Summary
Migrates the
getdescriptor onto a request-bound device runtime (ADR 0019 §6), Wave 4 of #1739.Cutover rule id R36.
Along the way this PR also became the home of the selector capture seam (because
getis itsfirst consuming command) and retired two legacy paths that the seam made retirable: the
readdispatch alias and
get's direct-iOS selector shortcut.Start record: #1739 (comment)
What lands here
1.
getis runtime-backed. It declaresselectorCaptureRuntimePlanUses— requiredcaptureSnapshot/captureSnapshotWithoutActiveApp, preferredreadTextAtPoint— resolvesits plan, inspects the exact owner's facts once, refuses before binding, and binds exactly once
through the
admitRuntimePlantoken.2. The selector capture seam. Extracted from #1876, which could not ship it standalone: with
find's cutover deferred it had no consuming command (ADR 0019 §10) and was not dead-code clean(
check:production-exports19 → 20).resolveBoundSelectorCapturereturns a record of boundoperations, so
readTextis a second member rather than a signature change.waitandisarestacked on this branch and consume it.
3. The
readdispatch alias is retired, in full. Read-onlyfindnow constructs a boundbackend, so
get textandfind <q> get textexecute the same boundreadTextAtPoint. With noconsumer left, the whole chain is deleted: the
readregistry entry and itsdispatch: {}projection,
DISPATCH_HANDLERS.read,handleReadCommand, the legacy adapter, the duplicatedplatform reader branches, and the now-empty
'dispatch-alias'catalog group.find's descriptorstays
LEGACY_PLATFORM_EXECUTION— only its read leg moved, and it claims no cutover row.Deleting the registry entry drops
'read'fromDescriptorDispatchCommandName, so a survivingDISPATCH_HANDLERS.readis a compile error rather than something R36 has to police.4. The direct-iOS selector shortcut is retired.
dispatchDirectIosSelectorGetreachedrunAppleRunnerCommandthrough a path R36 declares no operation for; admitting before a bypass isnot executing through the seam. Every
gettarget shape now resolves through the bound capture.queryDirectIosSelectoritself stays —offscreen-target probestill consumes it and itremains single-copy.
Cost, stated plainly:
get text id=…loses its tree-capture skip on iOS. A simple id selectorthat previously answered from a direct runner query now captures the tree and resolves through it.
ismeasured the comparable shortcut at 0.14 s held / 0.25 s failed. No fallback was added and thelatency is not recovered anywhere.
querySelectorreturns as a declared, fact-admitted, §9-measuredoperation in a later unit that also moves the offscreen probe, retiring the root function in one
move — the joint-retirement shape that worked for
read.Retired
getcapability bucket, andrequireCommandSupportedadmission forget;'get'fromHARMONYOS_SUPPORTED_COMMANDSandWEB_QUERY_COMMANDS;readchain (above);dispatchDirectIosSelectorGetand its two now-unreachable helpers;resolveBoundGetRuntime,get-runtime.tsand its test,elementReadRuntimeUse/elementReadRuntimePlan— all superseded by the seam rather than left alongside it;src/platform-runtime-element-text-host.tsand itselementTexthost wiring.Declared behavior changes
getmoves from capability-admitted-then-runner-failure to a typed unavailableadmission refusal — the same sentinel
snapshotanddiffalready landed.get texton an editable iOS node instead ofsilently returning stale captured-tree text. That is the point of retiring the generic catch.
get text id=…loses its fast path (above).Test fixture change that is not incidental churn
makeSessionininteraction-touch-fixtures.tsnow returnsmakeIosAppSession. On aniOS-family leaf the without-active-app capture row is unavailable, so a selector command run against
a session with no tracked app resolves the without-active-app plan and is refused at admission
before it captures, returning the shared
SESSION_NOT_FOUND"run open first". Reviewers shouldconnect this to the admission-refusal surface every selector command inherits, not read it as churn.
Validation
pnpm check:affected --run && git push: PASS — "all runnable checks passed".grep), with R36 and R39 both present and no silent merge.
fallow audit: clean on changed files.iPhone 17 Pro(F7D6F9A4-…, confirmed in the runner log):get text/get attrsover@refand selector, both not-found paths, and the §9 measurement —required path alone 511 chars on a
[truncated]node, bound preferred read 1,126 chars.Red-before-green was captured for every regression: R36's 5 pre-cutover violations, the
admission-ordering regression (
expected true to be false), the typed-outcome propagation(
promise resolved 'snap' instead of rejecting), and a plantedTS2322proving the exhaustivenessguard bites — re-proved after the refactor that removed the runtime reason list.
Accounting — read this before the size discussion
Production only (excluding tests, fixtures, docs), from the merge-base: +753 / −238 = net +515.
Two corrections to figures quoted earlier in review:
Interactorchange was expected to remove ~89 lines by deletingplatform-runtime-element-text-host.ts. It bought ~4, not ~89 (net +520 → +516 before a lintfix). The four readers still have to exist — they relocated into the Apple/Android/Linux
interactors, and the contract's binder absorbed the resolve/guard logic. Only the port vanished.
That change was made for consistency of mechanism (
findTextandreadTextAtPointnow reachtheir mechanics the same way, so Wave 5/6 retires one seam instead of two) and it delivers that
fully — but it should not be read as having delivered a size win it did not.
Per #1842, the CI Size report on this head is the authority;
pnpm size --base <ref>does notexist and local comparisons are not the evidence.
Structural outcomes from the size investigation:
ElementTextRuntimeExecutionwas a byte-identicalduplicate of
SnapshotRuntimeExecution(removed, reused);FrozenUnavailablePlatformRuntimeFactsisnow derived (
Required<Omit<…>>) rather than restated, so a future cell cannot be added to oneand forgotten in the other; and a stale comment claiming a duplication this branch had already
retired was removed.
Known gaps
captureSnapshotkeeps its optionalcaptureData?seam and thelegacy
captureSnapshotWithInteractorfallback.CaptureSnapshotInput.signalis not on this branch — it belongs towait(refactor: migrate wait to request-bound runtime #1875), along withthe regression proving per-poll abort and quiescence.
snapshotPlanUnavailableResponseis deliberately not exported; it lands withwait, itsfirst external consumer.
Docs and skills are unchanged: CLI grammar, help, and user-visible
getbehavior are identicalapart from the declared changes above.