refactor(capture-kit): lift the iOS snapshot route into the capture-kit router

Merged
TH
21h ago
+1284-1126

Summary

Step 3 of #4205. This PR moves the iOS snapshot route core into packages/capture-kit/src/snapshot-router/, the platform-neutral router from ADR 0038. Both iOS Simulator channels bind to it: the local guest bridge (createAppleSnapshotRoute) and the runner-served bridge (createRunnerSnapshotBridge). The Apple route core is deleted. No capture routes twice, no adapter wraps the old route, and no module re-exports a moved declaration.

Refresh design

The router keeps apart the two operations that origin/main kept apart:

  • Forced rediscovery. Every channel declares a required refreshTarget. The stale-target fallback always calls it. On origin/main both channels forced a rediscovery here. The first revision of this PR let the local channel use ordinary resolution, which could republish the cached generation after the bridge reported a newer one. This revision fixes that.
  • Refresh before a disabled circuit is trusted. Every channel declares a required resolutionVerifiesIdentity. Local: true, because its resolver checks the cached process start time. Runner: false, because its resolveTarget returns the cache without asking the runner. Only the runner refreshes before it trusts a disabled circuit, so the local channel keeps main's cost on that path.
  • The local resolver no longer takes a 'refresh' flag. It now has a separate refreshTarget, the same shape as the runner bridge. snapshot-target.test.ts calls refreshTarget and keeps its assertions.
  • New test in snapshot-route.test.ts: with the real local resolver, after a stale-target refusal (same pid and start time, new launch label) the fallback stamps the rediscovered generation. If the fallback uses resolveTarget, this test and the mocked stale-target test both fail (mutation checked). If either channel's resolutionVerifiesIdentity is flipped, 2 local route tests or 3 runner tests fail.

Shims removed

snapshot-source/types.ts and snapshot-source/errors.ts no longer re-export SnapshotSourceFailure, SnapshotSourceFailureKind, SnapshotSourceOutcome or SnapshotSourceError. The snapshot-source facade no longer re-exports the moved types, and its fallow exemption no longer lists SnapshotSourceFailureKind. Every importer now imports from @agent-device/capture-kit/snapshot-source-channel. The imports are type-only or reuse an edge that already existed through errors.ts, so the eager closure does not grow.

What has fewer owners or paths than origin/main

  • Constructors: 3 → 1. createSnapshotBridgeRoute, createSnapshotBridgeRouteCore and createAppleSnapshotRoute's use of the core → one createSnapshotRouter. createAppleSnapshotRoute is now only the local channel binding, about 70 lines (it was 833).
  • Channel declarations: the runner channel had one inline copy in runner-runtime.ts and three copies in runner-snapshot-bridge.test.ts. Now createRunnerSnapshotBridge returns the channel itself, so there is one declaration.
  • Host fields the route reads: 2 → 1. On main the route read snapshot.presentIosAcquisition and appleApplications.hasLiveRunnerSession, and the runner had to supply a live-session port. Now the router takes only the presenter function. The live-runner check is part of the local channel's awaitsDiscovery.
  • Refresh paths: main had a 'refresh' mode flag on both resolvers and also an optional refreshTarget? that the runner filled by calling its own resolver with the flag. Now each channel has one refreshTarget and no flag. When the router refreshes is one explicit declaration (resolutionVerifiesIdentity). It no longer depends on whether an optional member is present.
  • Discovery wait: main used the channel's isDiscoveryPending? and the host's hasLiveRunnerSession. Now it is one member, awaitsDiscovery?.

What did not get simpler: the lift itself did not change the settlement path count. Step 2 had already merged the fallback settlers into one settleFallback. Eligibility still has one owner, routeEligibility. That owner now reads servesDevice from the channel instead of calling hasSimulatorBridge directly. That member is a cost of platform neutrality: both channels pass the same function.

Validation

Tested SHA 7805744edd.

  • pnpm check:affected --run --head 7805744edd: all runnable checks passed (exactCommit=true). GitHub-authoritative checks were skipped locally.
  • vitest for capture-kit, platform-apple, test/repo-guards/snapshot-bridge-refusals.test.ts and the daemon snapshot-runtime-route.test.ts: all passed.
  • pnpm typecheck, pnpm lint, pnpm check:layering and node --test scripts/layering/*.test.ts (477) passed.
  • The router-boundary positive control now reads the tracked manifests and covers apple-runner and tv-cdp (Cubic). A control run that removed tv-cdp from the policy failed the test.
  • Live parity (local iOS Simulator, Limrun iOS): the coordinator reruns these checks.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.