refactor(ios): delete the unused snapshot plan interface - #2392
Conversation
`planIosSnapshot`, `IosSnapshotPlan`, the `plan` member of `IosSnapshotEngine` and `createIosSnapshotEngine` had no production caller: production reaches presentation through `publishIosSnapshot` / `presentIosSnapshot` directly, and the barrel re-export was all that kept the factory alive for fallow. Deleting the plan takes the last reader of most of `IOS_SNAPSHOT_PRODUCER_CAPABILITIES` with it. The table was typed over all four producers while only the two provider producers ever consumed its residue-shaping fields, and it had already drifted: it declared `simulator-ax-bridge` with `hittabilityEvidence: 'available'` while the bridge adapter emits `unavailable-fact: hittability` on every capture. Rather than correct the value, the table is now keyed on `IosProviderAcquisitionProducer`, so a producer that builds its own facts cannot declare one at all. Truncation is the one capability the runner and the bridge still need answered, so it moves to a table of its own over all four producers, read through `iosSnapshotTruncationEvidence`. Both keep `'available'`, which is what the adapter and the runner payload actually prove.
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed and independently verified. Three things I checked rather than took on trust: The 3 reported failures are contention, not regression — settled properly. Comparing failure counts across branches (5 on main vs 3 here) is a weak signal, so I re-ran all three named files serially on this branch instead: Including all three named tests. The box was still at load 52 with swap nearly full; The truncation split is behavior-preserving, by value. The new The drift channel is actually closed. Keeping −135 lines net. No behavior change, no allowlist, no compatibility re-export. |
|
Ready for human review at 3b6bbd2. The deleted plan and factory have no production callers, and the narrowed capability table preserves provider residue and all four truncation answers through the real snapshot command. No code findings; all required checks pass. |
|
…cope-contracts * origin/main: (25 commits) fix(ios): avoid duplicate alert routing queries (#2398) docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (#2355) chore(gates): classify #2278 daemon-platform-runtime edges and ratchet handler session authority (#2354) test(capture): shrink the 3x crop fixture to avoid coverage timeouts (#2399) perf(ios): settle the first interaction on a deadline, not a fixed sleep (#2395) feat: support standalone Maestro clearState command (#2366) refactor(capture-kit): complete ADR 0019 end state — relocate snapshot and recording zones (#2385) refactor(ios): delete the unused snapshot plan interface (#2392) fix(conformance): isolate post-tap settling from app launch (#2390) test(remote): assert the proxied snapshot's whole wire conversation (#2387) fix(daemon): stop an attested tenant from downgrading its own isolation (#2386) refactor(ios): prune converged snapshot paths (#2383) refactor(move): move the batch runner and batch policy into @agent-device/command-registry (#2388) fix(remote): let a plain-session client read its own failure record (#2382) refactor(move): move replay divergence vocabulary into @agent-device/ad-replay (#2384) refactor(cli): take the CLI's vocabulary off runtime barrels (#2379) perf(cli): keep scroll vocabulary off gesture runtime (#2378) chore: drop the stale planned-operations closure approval (#2373) fix(gates): stop an eager-closure approval from turning main red on merge (#2375) fix(ios): honor the startup budget through a cold Simulator boot (#2325) ...
Context: #2199, and the follow-up comment that found these two items by following #2383's failure mode — a capability declared for a producer that never consults it.
1. The
planhalf of the engine was production-deadEvidence that each deleted symbol had no live consumer, re-verified on
be622491f6:planIosSnapshotios-snapshot-engine/engine.ts:33(assigns it into the engine object) andios-snapshot-planning.test.tscreateIosSnapshotEngineios-snapshot-engine/index.ts:1andengine.test.ts:344engine.plan(...)grep -rn "\.plan(" src packages testreturns no hitsIosSnapshotPlanplanIosSnapshot's return type, and theplanmember ofIosSnapshotEngineIosSnapshotAcquisitionNarrowingIosSnapshotPlan.narrowingProduction reaches presentation through
publishIosSnapshot/presentIosSnapshotdirectly. Neither fallow gate saw it:createIosSnapshotEngineis reachable from a package subpath entry, which is what the "only entry surfaces re-export" rule exists to prevent. No ADR pins it — ADR 0004's "capture plan" is an ordered set of capture backends under a shared wall-clock budget, a different concept fromIosSnapshotPlan.Deleting the plan also takes the last reader of
scopeCompleteness,interactiveQueryCompletenessandviewportEvidence(onlyplanIosSnapshotever read them) and of thenot-applicabledepth-support variant (only theapple-runnerrow andplanIosSnapshot's presented branch ever built one), so those go with it.IosSnapshotCompletenesshad no remaining referent afterwards.I did not delete
IosSnapshotEngineitself — it is the closed-surface guard for "presentation happens exactly once" (#2188 invariant 2), pinned bypackages/contracts/src/ios-snapshot.test.ts. It now has one member, andengine.test.tspinspublishIosSnapshotto it so the contract describes the export production actually calls rather than a shape only the deleted factory satisfied.2. The seam for the capability table, and why drift becomes unrepresentable
IOS_SNAPSHOT_PRODUCER_CAPABILITIESwasRecord<IosSnapshotProducer, …>— all four producers — but its residue-shaping fields were consumed only byplanIosSnapshot(dead) and bycreateIosSnapshotAcquisition, whose parameter isIosProviderAcquisitionProducer. It had already drifted: it declaredsimulator-ax-bridgewithhittabilityEvidence: 'available'whilepackages/platform-apple/src/snapshot-source/adapter.ts:228emits{ kind: 'unavailable-fact', fact: 'hittability' }unconditionally. The residue is the truth; the declaration was wrong.Two tables now, each keyed on exactly the producers a consumer asks about:
IOS_PROVIDER_ACQUISITION_CAPABILITY_VALUES—Record<IosProviderAcquisitionProducer, IosProviderAcquisitionCapabilities>, module-private.apple-runnerandsimulator-ax-bridgehave no row, sohittabilityEvidencefor them does not merely become correct, it stops being expressible: adding one back is a compile error, not a wrong value. The drifted field is deleted rather than fixed, which is the point — fixing the value would leave the channel open. This table is also no longer exported, so a future reader cannot re-acquire it through the subpath the way the barrel keptcreateIosSnapshotEnginealive.IOS_SNAPSHOT_TRUNCATION_EVIDENCE—Record<IosSnapshotProducer, IosSnapshotEvidenceAvailability>, read throughiosSnapshotTruncationEvidence(producer). This is the one fact all four producers owe an answer to, so it is a table of its own instead of a column of the provider capabilities: every producer states it exactly once, in the only place that asks.deriveIosSnapshotAcquisitionResiduereads it from here too, so there is no second declaration to drift against.The
producer.stage === 'acquired'guard in the residue derivation went with the narrowing — every row in a provider-keyed table is acquired by construction, so the guard was a tautology.3. Proof that
snapshotTruncationForResultis unchangedapple-runnerandsimulator-ax-bridgeweretruncationEvidence: 'available'before and are'available'now, and that is correct:IosRunnerPayloadFacts.truncatedis a required boolean, and the bridge adapter rejects an envelope whosetruncatedis not a boolean (adapter.ts:178) and turns atrueinto atruncatedresidue. Two tests pin it:packages/capture-kit/src/ios-snapshot-planning.test.ts—iosSnapshotTruncationEvidencereturns the expected value for each of the four producers; the source table issatisfies Record<IosSnapshotProducer, …>, so a new producer cannot be added without declaring one.src/commands/capture/runtime/snapshot.test.ts— drives the realcapture.snapshotcommand with an absenttruncatedfor each of the four producers and asserts the observable result:falseforapple-runnerandsimulator-ax-bridge,undefinedforappium-sourceandlimrun-ios-tree. This extends the single pre-existing Appium case to the whole producer axis.No observable CLI behavior changes. The provider (Appium/Limrun) path keeps the table it legitimately uses, and its derived residue is unchanged — the residue test now covers both provider producers instead of Appium alone.
Gates
pnpm typecheckpnpm lintpnpm formatpnpm check:layeringpnpm check:fallowpnpm check:production-exportscreateIosSnapshotEnginepnpm check:affected --runNo
test/wire-compatchange was needed: the daemon RPC wire surface digests nothing underios-snapshot.ts, and no removed declaration is listed insurface.ts.The three
check:affectedfailurespnpm check:affected --runreports 3 failed / 2645 passed. All three are daemon dead-ownerreconciliation and runner-prewarm tests, none of which touch iOS snapshot presentation, the
capability table, or truncation:
src/daemon/__tests__/request-router-open.test.ts:: open reconciles a foreign dead owner through that owner state dirsrc/__tests__/daemon-entrypoint.test.ts:: startup sweep settles a foreign dead owner without touching a live same-named sessionsrc/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts:: prepare ios-runner starts the XCTest runner on an explicit iOS selectorThey reproduce on unmodified
be622491f6: running those three files on the base commit fails5 tests, versus 3 on this branch. Two consecutive runs of the full gate on this branch also
produced different failure sets (5 then 3), which is the machine's signature rather than a
regression — this box was at load average 60-95 with swap nearly full throughout.