Skip to content

refactor: PlatformPlugin registry foundation (step a) — Phase 3#956

Merged
thymikee merged 2 commits into
mainfrom
phase3-platform-plugin-foundation
Jun 30, 2026
Merged

refactor: PlatformPlugin registry foundation (step a) — Phase 3#956
thymikee merged 2 commits into
mainfrom
phase3-platform-plugin-foundation

Conversation

@thymikee

@thymikee thymikee commented Jun 30, 2026

Copy link
Copy Markdown
Member

Phase 3 — PlatformPlugin: the behaviorless foundation (step a only)

This is the safe, behaviorless scaffold for the platform-plugin axis of
plans/perfect-shape.md §5.1/§6 and ADR-0009. It adds a
PlatformPlugin registry that WRAPS today's existing interactor factories and device-discovery branches —
no leaf code is rewritten, default behavior is byte-identical. The risky remainder (steps b/c) is a
plan only below.

What this PR adds (scaffolding)

  • src/core/platform-plugin/plugin.ts — the PlatformPlugin type (type-only imports; lazy
    createInteractor/discoverDevices; capability.bucket). Per review, the contract is trimmed to only the
    facets this slice actually implements and parity-tests
    — the speculative providers/recording/appLog/
    perf daemon facets are NOT declared here (they are introduced in step b with platform-neutral,
    daemon-owned types, so the foundation never bakes in the old iOS-simulator recording shape). Plus the
    registry: registerPlatformPlugin, getPlugin (throws the same UNSUPPORTED_PLATFORM AppError as the
    old switch default), tryGetPlugin, registeredPlatforms.
  • src/core/platform-plugin/register-builtins.tsapple (owns ios+macos), android, linux,
    web plugins wrapping the existing core/interactors/* factories and platform-inventory.ts branches via
    the same lazy dynamic import()s. Includes the compile-time exhaustiveness assertion
    (BuiltinPluginsCoverAllPlatforms): a new Platform literal without a plugin fails the build.
  • src/core/interactors.tsgetInteractor now return getPlugin(device.platform).createInteractor(...)
    after the unchanged provider-device check. Verified byte-identical: same lazy imports, same factory calls,
    same throw code+message. This makes the registry production-live (not dead scaffold).
  • src/core/platform-inventory.ts — exports WEB_DESKTOP_DEVICE + shouldUseHostMacFastPath so the
    web/apple plugins reuse the same instance/predicate (no divergent copy).
  • src/core/platform-plugin/__tests__/parity.test.ts — the parity suite.

Placement note: the registry lives in src/core/platform-plugin/ (mirroring the existing
core/platform-descriptor/ and core/command-descriptor/ foundations), not §5.1's src/platforms/,
because everything it wraps today lives in core/ and that keeps every import in the allowed core→core /
core→platforms direction. Relocating it to src/platforms/apple/ is part of step (c).

Allow-lists: derived vs. left hand-authored

  • Left hand-authored (parity-tested, NOT derived): PLATFORMS (kernel/device.ts) and parsePlatform
    (utils/parsing.ts) stay the source of truth — per the roadmap's "err toward leaving hand lists." Nothing is
    derived FROM the registry yet; the parity test only PROVES equivalence.
  • Already derived (not a hazard): the CLI --platform enum reads PLATFORM_SELECTORS (cli-flags.ts).

Parity tests

  1. registeredPlatforms() is byte-for-byte equal to the canonical PLATFORMS tuple, in order.
  2. Registry coverage is byte-for-byte equal to the hand-authored parsePlatform accept-set (verbatim
    reference copy in-test, incl. the apple selector correctly excluded as a non-leaf).
  3. Every plugin capability.bucket matches the existing platformDescriptors registry (no drift between the
    two foundations).
  4. A family plugin resolves to the same instance for every leaf it owns (ios/macos → one Apple plugin).
  5. Each registered platform resolves to a plugin that lists it and exposes lazy methods.
  6. getPlugin throws UNSUPPORTED_PLATFORM (verbatim code+message) for an unregistered platform; duplicate
    registration is a hard error.

Local verification (all green)

  • tsc -p tsconfig.json ✅ · oxlint . --deny-warnings ✅ · oxfmt --check ✅ · rslib build
  • fallow audit --base origin/mainNo issues
  • vitest run --project unit over the new tests + core/ + platforms/__tests__/183 passed (incl. the
    existing getInteractor routing tests, unchanged through the registry).

Plan for the risky remainder — review the plan before starting steps b/c (do not auto-merge those)

Full file:line-grounded plan in
plans/phase3-platform-plugin-progress.md.

Step (b) — capability + daemon columns onto plugin grants: route isCommandSupportedOnDevice through
getPlugin(...).capability.bucket (pure swap); port every supports()/unsupportedHint() device closure
verbatim
(command-descriptor/registry.ts:41-54 — these encode macOS-coordinate-pinch, tvOS-no-touch,
two-finger synthesis nuance — never flatten to data); introduce the providers/recording/appLog/perf
facets typed against platform-neutral, daemon-owned wrappers of the existing start+stop contexts (de-iOS-
naming startIosSimulatorRecording) by wrapping the daemon branches (request-platform-providers.ts:117-233,
record-trace-recording-backends.ts:73-101, app-log.ts:179-185/344-375, session-perf.ts:109-131). Each
table pinned by a table-equivalence parity test BEFORE any hand table is deleted.

Step (c) — unwind macOS out of platforms/ios: relocate the ~797 LOC macOS leaf + the OS-agnostic engine
(6,136-LOC runner stack) into src/platforms/apple/{os/macos, core}, byte-identical, gated behind the
sim-validation request-counting harness
(runner request count unchanged before/after each commit), per
apple-platform-consolidation.md. The XCTest two-finger synthesis and adb/idb leaves stay untouched. The
ios+macosapple Platform collapse is the last, highest-diff step.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.4 MB 1.4 MB +1.1 kB
JS gzip 455.9 kB 456.1 kB +198 B
npm tarball 557.7 kB 557.9 kB +181 B
npm unpacked 2.0 MB 2.0 MB +1.1 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.3 ms 25.3 ms -0.0 ms
CLI --help 46.9 ms 46.0 ms -0.9 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/9722.js +1.1 kB +198 B

@thymikee

Copy link
Copy Markdown
Member Author

Careful draft review against plans/perfect-shape.md §5.1/§6, plans/apple-platform-consolidation.md, ADR-0009, and the linked core/daemon branches. The live getInteractor route looks behavior-preserving to me: provider devices still short-circuit first, the plugin factories keep the same lazy dynamic imports, and the Apple plugin correctly owns the current ios + macos leaves.\n\nOne foundation-shape blocker: PlatformPlugin.recording is already declared as { start(request: IosSimulatorRecordingRequest): RecordingProcess } in src/core/platform-plugin/plugin.ts. That bakes the old iOS-simulator provider seam into the platform-plugin contract, but the PR's own plan says step (b) will wrap resolveRecordingBackendForDevice / stopActiveRecording and de-iOS-name startIosSimulatorRecording. The current type cannot represent the existing Android, web, macOS-runner, iOS-device-runner, or stop-path contracts: those need the daemon recording context (active session, deps, fps flag, recording base, resolved output, and the recording tag for stop), not just { device, outPath } -> child/wait.\n\nPlease either remove the unpopulated daemon facets from this step-a contract until the neutral daemon-owned facet types exist, or type recording against a platform-neutral wrapper of the existing RecordingBackend/start+stop context. As-is, the scaffold would force step (b) to either widen the contract again or incorrectly flatten recording behavior, which is exactly what the plan says not to do.

Remove the speculative daemon-owned facets (providers/recording/appLog/perf)
from the PlatformPlugin type. The earlier 'recording' facet baked the
iOS-simulator provider seam (IosSimulatorRecordingRequest) into the contract and
could not represent the Android/web/macOS-runner/iOS-device-runner/stop-path
recording contracts, which need the daemon recording context. The step-a
contract now carries only what this slice implements and parity-tests:
id, platforms, familySelector?, createInteractor, discoverDevices, capability.
The facets are introduced in step (b) as platform-neutral, daemon-owned
wrappers, pinned by table-equivalence parity tests (plan updated).
@thymikee

Copy link
Copy Markdown
Member Author

Addressed the foundation-shape blocker (commit fce3c12) — applied option A: trimmed the step-a contract to only what this slice genuinely implements and parity-tests.

Removed the iOS-shaped recording facet — it baked the IosSimulatorRecordingRequest ({device,outPath} -> child/wait) provider seam into the contract and could not represent the Android / web / macOS-runner / iOS-device-runner / stop-path recording contracts (which need the daemon recording context: session, deps, fps, recording base, resolved output path, and the recording tag for stop). Premature, and would have forced step (b) to re-widen or flatten — exactly what the plan forbids.

Audited the other speculative facets (providers, appLog, perf): none were populated or routed through a real call-site, so all three were removed too. The PlatformPlugin type now contains ONLY: id, platforms, familySelector?, createInteractor, discoverDevices, capability { bucket, supportsByDefault? }. As a bonus, plugin.ts no longer imports from src/daemon at all (dropping the core → daemon type edge).

Verified the deletion was clean: grep confirmed no call-site reads plugin.recording / .appLog / .perf / .providers, and register-builtins.ts populated none of them.

Plan updated (plans/phase3-platform-plugin-progress.md): step (a) "Contract scope" now records why the iOS-shaped facet was dropped; step (b.3) is reworded from "populate already-declared facets" to "INTRODUCE the facets, typed against a PLATFORM-NEUTRAL, daemon-owned wrapper" — for recording, a daemon-owned RecordingBackend start+stop context carrying session/deps/fps/recording-base/resolved-output for start and the recording tag for stop (NOT IosSimulatorRecordingRequest), with startIosSimulatorRecording de-iOS-named there — each backend pinned by a table-equivalence parity test before any hand-table deletion.

All gates green after the change: tsc ✅ · oxlint --deny-warnings ✅ · oxfmt --write+--check ✅ · rslib build ✅ · fallow audit --base origin/main → No issues in 7 changed files ✅ · vitest (platform-plugin + platform-descriptor parity, interactors routing, provider-device, web-interactor) → 23 passed. Existing parity tests unchanged and still green. Stays a DRAFT.

@thymikee thymikee marked this pull request as ready for review June 30, 2026 12:16
@thymikee thymikee changed the title refactor: PlatformPlugin registry foundation — Phase 3 (DRAFT, human review) refactor: PlatformPlugin registry foundation (step a) — Phase 3 Jun 30, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed fce3c12 after the contract-scope fix and CI completion. The previous blocker is addressed: PlatformPlugin no longer declares the speculative daemon facets or imports daemon-owned recording/provider/log/perf types, and the follow-up plan now introduces those later as platform-neutral daemon-owned wrappers with parity gates. The live getInteractor route remains behavior-preserving, checks are green, and the PR is no longer draft. No remaining blockers; marking ready-for-human.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jun 30, 2026
@thymikee thymikee merged commit 531aad7 into main Jun 30, 2026
21 checks passed
@thymikee thymikee deleted the phase3-platform-plugin-foundation branch June 30, 2026 12:36
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-30 12:36 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.

1 participant