refactor: migrate get to the request-bound device runtime - #1877
refactor: migrate get to the request-bound device runtime#1877thymikee wants to merge 7 commits into
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.
`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.
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 |
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.