Repository navigation
refactor(ios): prune converged snapshot paths and close release gates #2199
Description
Activity
Prune audit re-checked against main, and the pruning PR
#2198 is closed, so this is unblocked. PR #2383 carries the prune. The old path list in this issue had drifted; re-audited item by item against
main@65ff27000b.The audit changed the shape of this issue
shouldPresentIosInteractiveSnapshot— renamedshouldPresentLegacyIosInteractiveSnapshot— was not dead code, and its live case was a bug.It fired when the channel was
xctest, the stageacquired, and the producer'spresentationOwnerwas notios-snapshot-engine.simulator-ax-bridgestill declaredpresentationOwner: 'snapshot-state'inIOS_SNAPSHOT_PRODUCER_CAPABILITIES— a value from #2233 that was never revised when #2197 routed the bridge through the engine. But the engine does present it:snapshot-route.ts→presentIosSnapshotAcquisition→publishIosSnapshot→engine.ts, which callsbuildIosInteractiveSnapshotPresentationwheneverinteractiveOnly.So on
main,snapshot --interactive-onlyon a local Simulator through the AX bridge ran iOS semantic compaction twice — a live violation of #2188 invariant 2 ("geometric presentation occurs exactly once"). I verified this independently of the PR: the capability table onmainand the engine's compaction call are both exactly as described.This makes #2383 a defect fix as well as a deletion, which is worth saying out loud since this issue was scoped as deletion-only.
Audit table
Delete (5): the legacy presentation branch; the direct
presentIosInteractiveSnapshotorchestration inbuildSnapshotState;presentationOwnerandIosSnapshotPresentationOwner;compactIosInteractiveSnapshot(a byte-identical alias with no production callers); thexctestterm inbackendScopesAfterWire.Retain, with justification (1): the remainder of
backendScopesAfterWire.linux-atspi,harmonyos-arkui,weband provenance-free captures have no in-projection scope pass, so--scopewould silently break for them. It was inverted from a denylist to an allowlist so iOS no longer appears in post-wire scope planning at all. I checked the equivalence rather than taking it on trust:SnapshotBackendis exactlyxctest | android | harmonyos-arkui | macos-helper | linux-atspi | web, so old and new are provably the same set — and a backend added later now defaults to no post-wire scope, which forces an explicit decision instead of silently acquiring one.STOP, per this issue's own stop conditions (2):
- Simulator observation depending on runner readiness. Owner:
resolveTargetForObservationinsnapshot-route.ts. ThehasLiveRunnerSessioncall chooses spin-vs-fallback; perf(ios): derive runner demand for Simulator opens and stop observation from awaiting the runner #2329 already removed the actual wait. Deleting the dependency is a behavior change, not a prune. - Repeated bridge retries after the generation circuit opens. Owner:
snapshot-observability.ts, re-exported ungated fromsnapshot-route.ts, withdisabledGenerationsliving in thecaptureclosure. Removing the retries means adding a circuit gate, which changes localopen/settle timing — perf(ios): optimize observation lifecycle and prove proxy parity #2198's surface, and out of bounds here.
Already gone (3): synthesized WebDriver hittability (the synthesis sits inside the
platform === 'android'arm; the iOS route cannot reach it); old iOS compaction locations (nothing duplicated); runner-local Swift geometric-presentation copies (one SwiftPM source of truth, consumed by local package reference).Exactly-once, made unrepresentable rather than detected
buildSnapshotStatenow takesSnapshotCaptureProvenance— the whole{backend, producer}pair or nothing — so no branch inside the assembly can rediscover who presented a tree. Requiringproducerglobally onSnapshotStateProvenancewas tried first and rejected: it breaks three legitimate client-side fallbacks that rebuild a state from a bareBackendSnapshotResultand genuinely do not know the producer. Tightening only the assembly seam broke test fixtures and nothing else, which is itself the proof that production never omits it.ios-snapshot-presentation-once.test.tspins both halves per producer, with a positive control that the fixture actually needs compaction.No-regrowth
New layering rule R74
snapshot-assembly-presentation-neutrality. All three forbidden directions were planted in the real tree and produced named::erroroutput before returning green. It closes a real gap: R73 coverspackages/provider-*but notpackages/platform-apple/src/snapshot-source/**. "Contracts cannot own algorithms" already belongs to R18 and was not duplicated. No allowlist was added — the one path that might have needed one was fixed at its declaration instead.Gates
pnpm check:affected --rungreen (751 files / 5,788 tests), plus typecheck, lint, format,check:layering,check:fallow,check:production-exports,check:daemon-wire-compat,check:di-seams,check:gate-manifest, and the test-file-size ratchet. No baseline regenerated or moved.Remaining for this issue
The final release evidence — package-size deltas against #2189, the conformance/fuzz/property corpus, provider contracts, and the Simulator/proxy runs — at a named final head once #2383 lands. #2189's package-size baseline is recorded at
71fb2483f: packed 982,960 B, clean-installed 3,347,347 B (436 files), bundled gzip 835,279 B (330 files). Note the PR Size Report bot measures installed including dependencies and is not comparable to those; the delta has to come from the bench harness's own package-size leg.- Simulator observation depending on runner readiness. Owner:
Two further prune items, found by following the #2383 defect's own failure mode
#2383 is merged. Its root cause was a capability declared for a producer that never consults it:
simulator-ax-bridgesaidpresentationOwner: 'snapshot-state'while the engine presented it anyway. Looking for other instances of that shape turned up two, and they are the same bug pattern rather than new ones.1. The
planhalf ofIosSnapshotEngineis production-deadplanIosSnapshot(packages/capture-kit/src/ios-snapshot-planning.ts:50) has no production caller. The only reference outside its own module isengine.ts:33, which assigns it to the engine object.createIosSnapshotEngine(ios-snapshot-engine/engine.ts:30) — the only thing that builds that object — has zero callers outside its own module and tests.engine.plan(...)is never invoked anywhere insrc/orpackages/.
Production reaches presentation through
publishIosSnapshot/presentIosSnapshotdirectly. SoIosSnapshotPlan,planIosSnapshot, andcreateIosSnapshotEngineare a declared interface half that nothing executes.Why the gates do not catch it:
createIosSnapshotEngineis re-exported from the package barrel (ios-snapshot-engine/index.ts:1), so fallow sees a package export and stops. It is not infallow-production-exports.json; the barrel alone keeps it alive. This is the hazard behind the "only entry surfaces re-export" rule.Not blocked by an ADR. ADR 0004's "capture plan" is a different concept — "an ordered set of capture backends under one shared wall-clock budget" (CONTEXT.md) — not
IosSnapshotPlan.2. The producer capability table has already drifted again
IOS_SNAPSHOT_PRODUCER_CAPABILITIESis typed over all four producers, but its residue-shaping fields are only consumed by:planIosSnapshot— dead, per above; andcreateIosSnapshotAcquisition, whose parameter isIosProviderAcquisitionProducer— i.e.appium-sourceandlimrun-ios-treeonly.
apple-runnerandsimulator-ax-bridgebuild their own residue and never read those fields. The only field still read for them anywhere istruncationEvidence, insnapshotTruncationForResult(src/commands/capture/runtime/snapshot.ts:232).Predictably, one has drifted. The table declares:
'simulator-ax-bridge': { …, hittabilityEvidence: 'available', … }
while the bridge adapter emits, unconditionally:
// packages/platform-apple/src/snapshot-source/adapter.ts:228 { kind: 'unavailable-fact', fact: 'hittability' } as const,
I confirmed which one is true on a device: every bridge-served
snapshot -ionad-bench-2198printed "iOS snapshot acquisition does not provide hittability evidence". The residue is right; the declaration is wrong.It is inert today —
engine.ts:119deriveshittabilityAvailablefrom the residue, not the table, and the table's reader is dead — so this is not a live user-facing defect. But it is a loaded landmine of exactly the kind that produced the double presentation: anyone consulting the table to ask "does the bridge provide hittability?" gets the wrong answer.Suggested shape
One deletion PR, in this issue's spirit ("no compatibility surface remains solely for the migrated implementation"):
- delete
planIosSnapshot,IosSnapshotPlan, theplanmember ofIosSnapshotEngine, andcreateIosSnapshotEnginewith its barrel export; - narrow the capability table to the producers that actually consume it, so a value cannot be declared for a producer that builds its own facts — the drifted
hittabilityEvidencethen stops existing rather than being corrected; - give
snapshotTruncationForResultitsapple-runner/simulator-ax-bridgeanswer explicitly, since that is the one field those two still need.
Fixing the value alone would leave the channel open, so this is the unrepresentable version rather than a detector.
- added a commit that references this issue
on Sep 22, 2026 Closed by #2750 (merge
ddc0d50b9e), measured at head7c434b5758, corpus tag
evidence/ios-snapshot/7c434b575/ evidence commit96d4951c19. CI on that head is green, including
Coverage, Integration Tests, Bundle Size, Repo Guards and the four Smoke matrices.Prune audit, checked against
origin/main:shouldPresentIosInteractiveSnapshotis gone;
presentIosInteractiveSnapshotexists only insidepackages/capture-kit/src/ios-snapshot-engine/
and its tests, with nothing left insrc/core;backendScopesAfterWiresurvives as the non-iOS
post-wire list (linux-atspi,harmonyos-arkui,web), which is the audit's intent restated at the
owning seam — iOS is kept out of post-wire scope planning entirely, and the list shrinks as channels
take ownership. Provider post-wire branches, synthesized WebDriver hittability, old compaction sites,
runner-local Swift geometry copies, and runner-readiness coupling in simulator observation went with
the earlier convergence PRs.Enforcement: R72 engine ownership, R73 provider presentation, R74 orchestration policy in
scripts/layering, each with planted-red tests.pnpm check:layeringand
pnpm check:fallow --base origin/maingreen at the merge.Size report: packed, clean-installed and bundled deltas against #2189 reported exactly in
docs/evidence/ios-snapshot-convergence-final-2026-09-21.md(+41–52%, across a 196-commit span, so
not attributable to this work).Open, deliberately outside this issue: #2751 — warm snapshot daemon cost measured ~2x the
baseline under a controlled side-by-side. The evidence sweep found it; it is a perf follow-up, not a
missing deliverable here.
Parent: #2188
Blocked by: #2195, #2198
Outcome
Delete the compatibility and backend-name policy that remains solely for the migrated iOS snapshot
implementation, enforce no-regrowth at the owning seams, and run the final package-size,
conformance, provider, Simulator, proxy, and affected release evidence.
This is a deletion and enforcement issue, not a place to finish incomplete behavior from blockers.
Each owning migration must already have removed its obvious local duplication; this issue audits and
closes the cross-cutting remainder.
Prune audit
Delete when equivalent engine-interface coverage is already proven:
shouldPresentIosInteractiveSnapshotfromsrc/core/snapshot-state.ts;backendScopesAfterWireand remaining backend-name scope planning;presentIosInteractiveSnapshotorchestration frombuildSnapshotState;capture-kitowns the implementation;generation circuit opens;
covered through the engine interface.
Move matching path-keyed fallow baselines rather than regenerating unrelated baselines. Stage new
structural modules before trusting the layering scan.
Do not delete
.xctestrunpreparation, or interaction protocol.adapters, golden fixtures, fuzz harnesses, live smokes, or physical-device code.
If deletion appears possible only by weakening another consumer, stop and report the remaining owner
rather than adding a compatibility re-export or fallback.
No-regrowth enforcement
contracts cannot own algorithms, and root orchestration cannot reconstruct engine policy.
invocation outside the engine.
Final evidence
source-line estimates. The compiled bridge cache remains outside the npm artifact.
structural gate with reproducible artifacts.
generation, fallback, and first-runner-interaction evidence.
and supported version-skew evidence.
pnpm check:affected --run; GitHub remains authoritative for native, provider, coverage, andfull macOS/Simulator lanes.
selection. Keep physical-iPhone behavior explicitly unchanged and separately owned.
Acceptance
met without wrong-tree, stale-tree, lease, or interaction regressions.
Worker stop conditions