Skip to content

refactor(ios): prune converged snapshot paths and close release gates #2199

Description

@thymikee

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:

  • shouldPresentIosInteractiveSnapshot from src/core/snapshot-state.ts;
  • backendScopesAfterWire and remaining backend-name scope planning;
  • direct presentIosInteractiveSnapshot orchestration from buildSnapshotState;
  • remaining provider-specific post-wire scope/option branches;
  • remaining synthesized WebDriver hittability;
  • old iOS semantic-compaction locations after capture-kit owns the implementation;
  • runner-local Swift geometric-presentation copies after the runner and harness use the shared source;
  • simulator observation dependencies on runner readiness and repeated bridge retries after the
    generation circuit opens;
  • tests whose only purpose was to assert removed producer-name branches, after equivalent behavior is
    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

  • XCTest tree, query-sweep, or private-AX capture backends.
  • Apple runner manager/cache, .xctestrun preparation, or interaction protocol.
  • Swift geometric presentation or TypeScript iOS semantic compaction.
  • Generic scope/normalization, refs, quality, occlusion, Android policy, provider acquisition
    adapters, golden fixtures, fuzz harnesses, live smokes, or physical-device code.
  • Proxy routes, device leases, authentication, artifact transfer, or RPC compatibility machinery.

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

  • Add or update structural ownership tests so producer adapters cannot import presentation behavior,
    contracts cannot own algorithms, and root orchestration cannot reconstruct engine policy.
  • Reject old path recreation, backend-name scope/presentation branching, or direct iOS presenter
    invocation outside the engine.
  • Plant each new forbidden direction/path and record the named red failure before returning green.
  • Do not add allowlists for unclassified paths; fix the declaration or ownership seam.

Final evidence

  • Compare packed, clean-installed, and bundled npm sizes with test(ios): establish snapshot convergence baselines and permanent evidence #2189. Report exact deltas; do not use
    source-line estimates. The compiled bridge cache remains outside the npm artifact.
  • Run the independent Swift/TypeScript golden corpus, bounded differential fuzz, properties, and
    structural gate with reproducible artifacts.
  • Run Appium and Limrun provider contracts on the exact head.
  • Run local Simulator cold-cold, cold, warm, relaunch, bridge crash/timeout/cancellation, stale
    generation, fallback, and first-runner-interaction evidence.
  • Run direct/proxy semantic parity, controlled-RTT performance, lease isolation/expiry, cancellation,
    and supported version-skew evidence.
  • Run pnpm check:affected --run; GitHub remains authoritative for native, provider, coverage, and
    full macOS/Simulator lanes.
  • Update CLI help/user docs only if observable behavior changed. Do not document internal producer
    selection. Keep physical-iPhone behavior explicitly unchanged and separately owned.

Acceptance

  • No compatibility surface remains solely for the migrated implementation.
  • Old paths and forbidden dependency directions have observed planted-red no-regrowth proof.
  • All child acceptance evidence is linked from iOS snapshot backend convergence and fast Simulator observation #2188 and corresponds to exact implementation heads.
  • No open blocker is represented as passing through fixture-only or stale evidence.
  • The size delta is within the maintainer-accepted budget and the remote/local performance targets are
    met without wrong-tree, stale-tree, lease, or interaction regressions.

Worker stop conditions

Activity

  1. thymikee commented on Sep 7, 2026

    @thymikee
    MemberAuthor

    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 — renamed shouldPresentLegacyIosInteractiveSnapshot — was not dead code, and its live case was a bug.

    It fired when the channel was xctest, the stage acquired, and the producer's presentationOwner was not ios-snapshot-engine. simulator-ax-bridge still declared presentationOwner: 'snapshot-state' in IOS_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 calls buildIosInteractiveSnapshotPresentation whenever interactiveOnly.

    So on main, snapshot --interactive-only on 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 on main and 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 presentIosInteractiveSnapshot orchestration in buildSnapshotState; presentationOwner and IosSnapshotPresentationOwner; compactIosInteractiveSnapshot (a byte-identical alias with no production callers); the xctest term in backendScopesAfterWire.

    Retain, with justification (1): the remainder of backendScopesAfterWire. linux-atspi, harmonyos-arkui, web and provenance-free captures have no in-projection scope pass, so --scope would 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: SnapshotBackend is exactly xctest | 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):

    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

    buildSnapshotState now takes SnapshotCaptureProvenance — the whole {backend, producer} pair or nothing — so no branch inside the assembly can rediscover who presented a tree. Requiring producer globally on SnapshotStateProvenance was tried first and rejected: it breaks three legitimate client-side fallbacks that rebuild a state from a bare BackendSnapshotResult and 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.ts pins 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 ::error output before returning green. It closes a real gap: R73 covers packages/provider-* but not packages/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 --run green (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.

  2. thymikee commented on Sep 8, 2026

    @thymikee
    MemberAuthor

    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-bridge said presentationOwner: '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 plan half of IosSnapshotEngine is production-dead

    • planIosSnapshot (packages/capture-kit/src/ios-snapshot-planning.ts:50) has no production caller. The only reference outside its own module is engine.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 in src/ or packages/.

    Production reaches presentation through publishIosSnapshot / presentIosSnapshot directly. So IosSnapshotPlan, planIosSnapshot, and createIosSnapshotEngine are a declared interface half that nothing executes.

    Why the gates do not catch it: createIosSnapshotEngine is re-exported from the package barrel (ios-snapshot-engine/index.ts:1), so fallow sees a package export and stops. It is not in fallow-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_CAPABILITIES is typed over all four producers, but its residue-shaping fields are only consumed by:

    • planIosSnapshot — dead, per above; and
    • createIosSnapshotAcquisition, whose parameter is IosProviderAcquisitionProducer — i.e. appium-source and limrun-ios-tree only.

    apple-runner and simulator-ax-bridge build their own residue and never read those fields. The only field still read for them anywhere is truncationEvidence, in snapshotTruncationForResult (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 -i on ad-bench-2198 printed "iOS snapshot acquisition does not provide hittability evidence". The residue is right; the declaration is wrong.

    It is inert today — engine.ts:119 derives hittabilityAvailable from 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, the plan member of IosSnapshotEngine, and createIosSnapshotEngine with 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 hittabilityEvidence then stops existing rather than being corrected;
    • give snapshotTruncationForResult its apple-runner / simulator-ax-bridge answer 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.

  3. thymikee commented on Sep 22, 2026

    @thymikee
    MemberAuthor

    Closed by #2750 (merge ddc0d50b9e), measured at head 7c434b5758, corpus tag
    evidence/ios-snapshot/7c434b575 / evidence commit 96d4951c19. 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: shouldPresentIosInteractiveSnapshot is gone;
    presentIosInteractiveSnapshot exists only inside packages/capture-kit/src/ios-snapshot-engine/
    and its tests, with nothing left in src/core; backendScopesAfterWire survives 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:layering and
    pnpm check:fallow --base origin/main green 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions