refactor(ios): prune converged snapshot paths - #2383
Conversation
The daemon snapshot assembly no longer presents. `shouldPresentLegacyIosInteractiveSnapshot` fired whenever an xctest capture arrived without a producer, or with a producer whose capability table still named `snapshot-state` as its presentation owner — which `simulator-ax-bridge` still did after routing moved it onto the engine, so a bridge capture with `--interactive-only` ran the iOS semantic presentation twice (#2188 invariant 2). Rather than deleting a runtime guard and hoping, `buildSnapshotState` now takes `SnapshotCaptureProvenance`: a capture either knows nothing about its origin or carries the whole pair, so the producer-less branch does not compile. Requiring the pair broke only test fixtures, which is the proof that production never omitted it. `presentationOwner` had one value left once the bridge was accounted for, so the capability and its type are gone; the truncation verdict that read it now reads `truncationEvidence`, which is the fact it was standing in for and matches it producer for producer. Post-wire scope planning names the channels that still need the pass instead of excluding the ones that do not, which takes iOS out of it. `compactIosInteractiveSnapshot` was a byte-identical alias of `presentIosInteractiveSnapshot` with no production caller. R74 holds it: the assembly and the Simulator bridge producer adapter may not import iOS presentation, and the assembly may not name the iOS channel or a producer.
Size Report
Startup median (7 runs, lower is better):
|
|
No code findings at 730c33a: presentation stays in the engine, and the new regressions cover the removed assembly pass. All checks now pass, including the live iOS fixture run with snapshot -i. Before marking this ready, please confirm from the run evidence that the interactive snapshot used simulator-ax-bridge rather than XCTest fallback; that is the producer whose extra presentation pass changed. The final size/conformance/provider/proxy release evidence remains tracked in #2199, so this pruning PR should not close that issue by itself. |
|
Confirmed — and worth saying first that the CI evidence could not answer this. The iOS smoke lane records no producer anywhere in its log (I searched the full run for Live run on this PR's head
Both captures returned 26 nodes, exit 0. Direct evidence it was the bridge, not XCTest fallback — the XCTest runner's own log for that session, after two The runner was started and ready ( Corroborating fingerprint. Both captures emitted:
That warning comes from the So the interactive snapshot ran through Independently of the live run, the routing is also pinned at unit level in On #2199Agreed, and it is already the case mechanically: this PR has no closing keyword and GitHub reports no linked closing issue for it, so merging it will not close #2199. The final size/conformance/provider/proxy release evidence stays tracked there — #2189's package-size baseline for that comparison is I am separately diffing |
|
| Leg | Producer | Nodes | vs head |
|---|---|---|---|
| main, attempt 1 | XCTest fallback | 33 | not comparable |
| main, attempts 2-4 | simulator-ax-bridge |
26 | byte-identical ×3 |
| this head | simulator-ax-bridge |
26 | — |
The first attempt is worth reporting rather than discarding
My first main capture came back with 33 nodes, and the naive reading — "the fix changes output by 7 nodes" — would have been wrong. It carried:
Simulator AX snapshot unavailable (application-server-unavailable); used XCTest for this app generation.
It had fallen back. Re-running until the bridge served it gave 26 nodes, byte-identical to this head, three times running.
Two things follow. First, the fallback is genuinely reachable right after a relaunch, which is exactly why asking to confirm the producer was the right call — a green run proves nothing about which path executed. Second, the two producers' output is visibly different (33 vs 26 nodes, different structure), so the producer fingerprint discriminates cleanly rather than being a subtle distinction.
Scope of the claim
Idempotent on this tree. I am not claiming iOS interactive compaction is idempotent in general — action-shelf retention and row collapse are the rules most likely to differ on a second pass, and this screen may simply not exercise them. That is why the guard belongs where this PR puts it: ios-snapshot-presentation-once.test.ts pins exactly-once per producer regardless of whether a second pass would have been visible, and R74 stops the assembly from acquiring a presentation pass again.
So the honest summary of the defect: on main, a bridge-served snapshot -i ran iOS semantic compaction twice; on this screen the second pass changed nothing observable, but it was wasted work and a standing violation of #2188 invariant 2, and nothing guaranteed it would stay invisible.
Raw outputs and the fallback transcript are in my scratchpad; happy to attach them to evidence/ios-snapshot alongside the #2199 final corpus if you want them as durable evidence.
Executes the pruning audit of #2199 against
mainat 65ff270. Deletion only: no compatibility re-export, no fallback, no allowlist. Context: #2199, tracker #2188.What the audit found
shouldPresentLegacyIosInteractiveSnapshotwas not dead. It fired for two live cases:provenance.producer === undefinedon thexctestchannel (stage falls back to'acquired', owner falls back toundefined);simulator-ax-bridge, whose capability table still declaredpresentationOwner: 'snapshot-state'— a value introduced by 7ee1a5d (refactor(ios): carry provider acquisitions through one presentation owner #2233), which movedappium-source/limrun-ios-treeonto the engine and left the bridge behind, and never revised once routing put the bridge on the engine too.The bridge is presented by the engine today:
packages/platform-apple/src/snapshot-route.ts:124callshost.snapshot.presentIosAcquisition→src/snapshot/ios-snapshot-runtime.ts:45publishIosSnapshot→packages/capture-kit/src/ios-snapshot-engine/engine.ts:135, which appliesbuildIosInteractiveSnapshotPresentationforinteractiveOnly. The assembly then applied the same rules a second time. That is a live violation of #2188 invariant 2 onmainforagent-device snapshot --interactive-onlyon a local Simulator through the bridge route.Per-item audit
shouldPresentIosInteractiveSnapshot(renamedshouldPresentLegacyIosInteractiveSnapshot,src/core/snapshot-state.ts:129)produceris now required on a capture, andpresentationOwneris gone. Coverage moved tosrc/core/__tests__/ios-snapshot-presentation-once.test.tsover all four producers.presentIosInteractiveSnapshotorchestration inbuildSnapshotState(:61)presentationOwnercapability +IosSnapshotPresentationOwner(packages/contracts/src/ios-snapshot.ts:18,89)snapshotTruncationForResult, now readstruncationEvidence— the fact it was standing in for. Producer-for-producer identical: apple-runnerfalse, simulator-ax-bridgefalse, appium-sourceundefined, limrun-ios-treeundefined.compactIosInteractiveSnapshot(ios-snapshot-engine/engine.ts:74)presentIosInteractiveSnapshot, exported from the same index, zero production callers; the one test re-points at the surviving export. This is the "old semantic-compaction location" the prune list names, in duplicate-entrypoint form.backendScopesAfterWire— thexctesttermbackendScopesAfterWire— the restlinux-atspi,harmonyos-arkui,weband provenance-free captures hand over an unscoped tree and have no in-projection scope pass;--scopewould silently stop working for them. Behaviour is bit-identical to the blocklist it replaces (exact complement over the six channels). Removing it needs those channels to take scope ownership, which is their own work, not a deletion here.packages/provider-webdriver/src/webdriver-source.ts:187-201: thevisibleToUser && enabled && rectsynthesis is inside theplatform === 'android'arm. The iOS arm (reportedHittabilityFact,:212-217) omits the key entirely when Appium did not report it. The iOS route (webdriver-interactor.ts:308→webdriver-ios-snapshot.ts:21) cannot reach the Android arm.collectIosRowPresentation/…Transition…/…NoiseSuppression/…WebSemantic…/reindexSnapshotNodesWithSuppressedParentsexists.src/snapshot/snapshot-presentation/ios/holds no implementation — four test/fixture files that import the capture-kit engine. See "not relocated" below.apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/. The runner consumes it as anXCLocalSwiftPackageReference(AgentDeviceRunner.xcodeproj/project.pbxproj:163-166) and calls into it (RunnerTests+Snapshot.swift:166,223,232); no source-level copy.Package.runner.swiftis a trimmed manifest swapped in byscripts/package-apple-runner-source.mjs:9-11, not a code copy.packages/platform-apple/src/snapshot-route.ts:153-170resolveTargetForObservation. #2329 already removed the wait; the survivinghasLiveRunnerSessioncall at:167decides whether a pending discovery keeps spinning or hands to the XCTest fallback. Deleting it makes observation spin when a live runner could answer at once. Covered bysnapshot-route.test.ts:240,338-360.packages/platform-apple/src/snapshot-observability.ts:61-74, re-exported ungated atsnapshot-route.ts:63. The circuit (disabledGenerations) lives in thecaptureclosure, soawaitObservablere-polls a disabled generation for the full transition window (~33source.acquireround trips atOBSERVATION_POLL_MS = 150). Removing the retries means adding a circuit gate to the probe, which changes localopen/settle timing — #2198's optimization surface, and a platform contract change this issue's stop conditions forbid. Filed as a follow-up rather than done here.snapshot-state.test.tscases whose subject was the presentation-owner branch are gone, superseded by the per-producer exactly-once suite.snapshot-publication-membership.test.tsandsnapshot-presentation-transitions.test.tsreached presentation through the legacyproducer === undefineddoor; both now run the production order — engine presents, assembly publishes.Deliberately not touched (#2199 "Do not delete")
XCTest tree / query-sweep / private-AX backends, the Apple runner manager/cache/
.xctestrun/interaction protocol, Swift geometric presentation, TypeScript iOS semantic compaction, generic scope/normalization/refs/quality/occlusion, Android policy, provider acquisition adapters, proxy routes, leases, auth, RPC compatibility. Physical-iPhone behaviour is unchanged: the'xctest'literals inpackages/platform-apple/src/core/physical-device-control.tsandrunner/runner-usbmux*.tsareIosPhysicalDeviceBackendvalues andAppErrordetail tags — a different type that happens to share the string, and untouched.Not relocated, with reason
src/snapshot/snapshot-presentation/ios/(4 test/fixture files) reads like a stale implementation home but holds only coverage of the capture-kit engine.transitions.fixtures.tsis shared withsrc/daemon/__tests__/snapshot-presentation-transitions.test.ts, whose first case is a daemon-assembly test — moving the fixture intopackages/capture-kitwould force a cross-package deep import (R11) or a split of that test. That is a test-ownership refactor, not a prune.Exactly-once proof
src/core/__tests__/ios-snapshot-presentation-once.test.ts, 12 cases.Presentation. Two halves, both pinned per producer:
presentIosInteractiveSnapshot, forsimulator-ax-bridge,appium-source,limrun-ios-treedriven through the realpresentIosSnapshotAcquisitionpath;buildSnapshotStatereturns the same(type, label)sequence it was given, for all four producers includingapple-runner.A positive control (
the collapsible fixture is not already a presentation fixed point) asserts the fixture genuinely needs compaction, so "already compacted" cannot pass by the rules having stopped matching.Scope. Per producer,
buildSnapshotStateis handedsnapshotScope: 'no-such-scope'on an xctest tree. A second post-wire pass would returnscopeSnapshotNodes' empty no-match slice (snapshot-desktop-projection.ts:44); the tree comes back whole. Same probe coversbackendScopesAfterWire's deletedxctestterm.Unrepresentability.
buildSnapshotStatetakesSnapshotCaptureProvenance(whole pair or nothing), so theproducer === undefinedcase cannot be constructed. Enumerating every xctest snapshot-provenance construction site backs this:src/snapshot/ios-snapshot-runtime.ts:47-49(producer: acquired.acquisition.producer) andpackages/platform-apple/src/interactor.ts:250-251(producer: 'apple-runner') are the only two, and both always set it —SnapshotResultrequiresSnapshotProvenanceat the interactor boundary. Tightening the assembly's input broke only test fixtures, which is the evidence.SnapshotStatekeeps its optional producer for the three client-side fallbacks that rebuild a state from a bareBackendSnapshotResult(commands/capture/runtime/snapshot.ts:190,interaction/runtime/resolution.ts:700,selector-read-shared.ts:101) and genuinely do not know it; those never reach the assembly.Planted red
Exactly-once tests, against the restored double presentation
Restoring the deleted branch in
buildSnapshotState:Note the composed-path case still passed under the regression: on that fixture a second pass over an already-presented tree is idempotent. The invariant that catches it is the stronger one — the assembly presents at all — which is why it is asserted directly rather than by output comparison alone.
R74, against all three forbidden directions planted in the real tree
node scripts/layering/check.ts:scripts/layering/snapshot-assembly-presentation-policy.test.tsplants each direction independently and adds a fail-closed case: if the assembly moves out from underSNAPSHOT_ASSEMBLY_FILES, the rule reports rather than passing over an empty file set.No allowlist was added. The one path that could have needed one —
backendScopesAfterWire— was fixed at the declaration instead, by naming the channels that still need post-wire scope so the iOS name disappears from the assembly entirely.Ownership coverage, and what was already enforced
packages/provider-*; R74 (new) forpackages/platform-apple/src/snapshot-source/**, the Simulator bridge adapter R73 did not reachcontracts-implementation-authorityalready bans non-type statements inpackages/contracts/src/ios-snapshot.ts; not duplicatedGates
pnpm typecheckpnpm lintpnpm format/format:checkpnpm check:layeringpnpm check:fallowpnpm check:production-exportspnpm check:daemon-wire-compattest/wire-compat/surface.tswas removed or changed, so no ledger editpnpm check:di-seamspnpm check:gate-manifestpnpm check:affected --runall runnable checks passed(751 files / 5788 tests)No fallow baseline was regenerated and none needed moving: nothing deleted here is path-keyed in
fallow-baselines/orfallow-production-exports.json. Native, provider, coverage and full macOS/Simulator lanes remain GitHub-authoritative.Not done here, and why
The two STOP items in rows 10 and 11 are the only prune-list entries left standing. Both are Simulator observation concerns owned by
packages/platform-apple/src/snapshot-route.tsandsnapshot-observability.ts, and closing either means adding behaviour (a circuit gate on the probe) rather than deleting a converged path — #2198's surface, and a change to localopen/settle timing that #2199's stop conditions put out of bounds.