Skip to content

refactor: collapse public Platform ios/macos into apple (#979)#1002

Merged
thymikee merged 3 commits into
mainfrom
phase3-d3-platform-collapse
Jul 1, 2026
Merged

refactor: collapse public Platform ios/macos into apple (#979)#1002
thymikee merged 3 commits into
mainfrom
phase3-d3-platform-collapse

Conversation

@thymikee

@thymikee thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes #979 (Phase 3 d.3, part of #972). Human-review-only — do not merge without review (public API surface).

What this does

Collapses the internal Platform union from ios/macos into a single apple platform, with appleOs as the sole OS discriminant (ADR-0009). Every discovered/persisted Apple device is now platform: 'apple' internally.

Approach (b): NON-BREAKING

The public contract is unchanged for machine consumers:

  • READ paths still accept the legacy ios/macos strings AND apple.
    • matchesPlatformSelector (device-aware now): --platform ios = every Apple OS except the macOS host; --platform macos = the macOS host only; --platform apple = all Apple. Byte-identical device sets to before.
    • parsePlatform (client response parser) and REPLAY_METADATA_PLATFORMS accept apple additively; replay context platform=apple|ios|macos all resolve.
    • CLI --platform / MCP enum already carry apple via PLATFORM_SELECTORS.
  • OUTPUT still emits the LEAF string (ios/macos), never apple. A single publicPlatformString(device) projection is applied at every emit site:
    • devices and session_list (daemon/handlers/session-inventory.ts)
    • boot / shutdown / appstate (daemon/handlers/session-state.ts) and prepare ios-runner (daemon/handlers/session.ts)
    • the backend platform (selector-runtime-backend, screenshot-runtime, snapshot-runtime, interaction-runtime) — AgentDeviceBackendPlatform is now PublicPlatform
    • the proxy device key (connection-runtime), request-lock backfill (request-lock-policy), runtime set binding (session-runtime / session-runtime-command), click-button validation (dispatch-interactions / interaction-touch)
    • both .ad context-line writers (replay/script.ts heal-write + daemon/session-script-writer.ts)
    • client-facing types stay PublicPlatform (client-types, contracts/device, parsing); checked-in .ad fixtures untouched.

Internal migration

  • Discovery SET sites → platform: 'apple' (+ appleOs); PLATFORMS['apple','android','linux','web']; platformDescriptors two Apple rows → one apple row; applePlugin.platforms['apple'].
  • ~125 open-coded device.platform === 'ios'|'macos' branch sites migrated to isIosFamily / isMacOs (behavior-preserving; isIosFamily is the exact post-collapse equivalent of the old platform === 'ios'). isMobilePlatform and isMacOs are appleOs-aware; isMacOs also honors a legacy leaf platform: 'macos' for persisted-record back-compat.
  • The selector engine (selectors-*, snapshot-processing, selector-is-predicates) only branches on 'android', so its platform params are widened to Platform | PublicPlatform (no projection needed).

Parity gate

src/kernel/__tests__/platform-collapse-parity.test.ts pins:

  • --platform apple and --platform ios resolve to the same device;
  • publicPlatformString emits the pre-collapse leaf for every fixture (incl. iPadOS/tvOS/visionOS → ios, and legacy leaf records);
  • selector membership for apple/ios/macos;
  • deviceFieldsFromPublicPlatform inverts the projection (round-trip);
  • the .ad heal-write emits the leaf and round-trips through the reader.

Verification (all green)

tsc --noEmit · oxlint . --deny-warnings · oxfmt --check src test · layering guard · rslib build · vitest run --project unit (2960 tests; the android fillAndroid case is a known CPU-contention flake — passes in isolation and on re-run) · node --test test/integration/installed-package-metro.test.ts · fallow audit --base origin/main (no new findings).

Deferred (unchanged by design)

Surfacing appleOs on the public devices shape (still stripped) is left as a deliberate later change. parsePlatform's appleios fallback is forward-compat only (the daemon never emits apple under approach b).

Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.

Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
  PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
  post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
  projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).

Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.

Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.

Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).

Refs #979 (part of #972).
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.5 MB 1.5 MB -639 B
JS gzip 477.5 kB 477.9 kB +389 B
npm tarball 578.3 kB 578.7 kB +383 B
npm unpacked 2.0 MB 2.0 MB -382 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.7 ms 25.9 ms +0.2 ms
CLI --help 47.1 ms 46.7 ms -0.4 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/cli.js +33 B +45 B
dist/src/session.js -277 B +34 B
dist/src/interaction.js +42 B +32 B
dist/src/selector-runtime.js +8 B +28 B
dist/src/record-trace-recording.js +1 B +25 B

@thymikee

thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

CI is blocked on Coverage and Integration Tests. The failing provider-integration assertions look directly tied to the public Platform ios/macos -> apple collapse:

  • test/integration/provider-scenarios/ios-alert-settings.test.ts:96 expects the opened app result to preserve the selected provider device id (sim-1), but open.device?.id is now undefined.
  • test/integration/provider-scenarios/macos-desktop.test.ts:54 expects the macOS lifecycle provider call key prepare:macos:desktop, but the branch now emits prepare:apple:desktop.

Please make the intended provider boundary explicit: either preserve the leaf platform identity where provider/device lifecycle semantics require it, or update the provider contract/tests with clear compatibility rationale before re-running CI.

…979)

The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)

Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.

Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.
@thymikee

thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Pushed a fix (head 2aa0399c0). Root cause of both failing jobs (Integration Tests + Coverage both run the provider-integration project):

  • ios-alert-settings open.device?.id was undefined — a real bug: session-open-surface.ts emitted the raw internal device.platform ('apple') in the open result; the client normalizer gates on isPublicPlatform (which excludes 'apple') and dropped the device. Fixed to project via publicPlatformString. Found + fixed the same leak in the session-perf response builders.
  • macos-desktop prepare:apple:desktop — this one is correct post-collapse: the mock provider observes the INTERNAL DeviceInfo (platform:'apple', appleOs:'macos'), so I updated the test expectation (with a comment). Providers legitimately see the collapsed internal platform.

Local: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/fallow green. Verifying on CI now; will confirm Integration + Coverage specifically before this is considered ready.

@thymikee

thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Review finding:

The public-output projection misses the nested perf memory support payload. buildBasePerfMemoryResponse() now correctly emits platform via publicPlatformString(session.device), but response.support and artifact.support still come from buildMemorySnapshotSupport() -> buildAppleMemorySnapshotSupport(), which returns platform: device.platform. After this PR Apple sessions have internal device.platform = apple, so perf memory snapshot/unsupported responses can emit support.platform = apple even though the PR approach says machine outputs still emit leaf ios/macos, never apple.

Please project the support payload through publicPlatformString(device), or make the nested field explicitly internal and document/test that, and add coverage for Apple perf memory support so this output contract stays pinned.

The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:

- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
  response.support.platform / artifact.support.platform, plus the
  sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
  cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
  snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
  target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
  Apple devices (reachable via deviceFieldsFromPublicPlatform).

Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
@thymikee

thymikee commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Review follow-up: the perf memory support leak is fixed in head 35e5d06, including coverage for iOS simulator, physical iOS, and macOS support payloads. I also spot-checked the follow-up projections for xctrace perf, doctor evidence, snapshot diagnostics, provider error details, and the scoped snapshot script path; I do not see another public apple-platform leak from this delta.

Checks are green (21/21). This looks ready for human maintainer judgment. Residual risk: I did not run an additional local simulator/device pass beyond the PR CI/smoke coverage.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 1, 2026
@thymikee thymikee merged commit cd1551b into main Jul 1, 2026
21 checks passed
@thymikee thymikee deleted the phase3-d3-platform-collapse branch July 1, 2026 17:29
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-01 17:30 UTC

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

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 3 d.3: final public Platform collapse ios/macos -> apple

1 participant