Skip to content

refactor: route capability bucket through PlatformPlugin + pin supports() closures (Phase 3 step b)#965

Merged
thymikee merged 1 commit into
mainfrom
phase3-apple-step-b
Jun 30, 2026
Merged

refactor: route capability bucket through PlatformPlugin + pin supports() closures (Phase 3 step b)#965
thymikee merged 1 commit into
mainfrom
phase3-apple-step-b

Conversation

@thymikee

Copy link
Copy Markdown
Member

Phase 3 step (b.1) + (b.2) — parity-gated capability migration onto PlatformPlugin

Routes the per-platform capability bucket through the merged PlatformPlugin registry (step a / PR #956) and pins the device supports() / unsupportedHint() closures, parity-gated and byte-identical. Scope is strictly b.1 + b.2 — no daemon facets (b.3) and no macOS unwind (c). device.platform stays ios/macos on the wire; no leaf code (XCTest synthesis, adb/idb, tvOS focus) touched.

What I migrated (b.1 — bucket source unified)

src/core/capabilities.ts isCommandSupportedOnDevice now selects the per-platform capability bucket via getPlugin(device.platform).capability.bucket (the single PlatformPlugin registry) instead of the deriveCapabilityForPlatform(platformDescriptors, …) fold. This makes the plugin registry the single bucket source (perfect-shape §5.1's const m = cap[plugin.capability.bucket]).

  • The former selectCapabilityForPlatform helper and the platformDescriptors / deriveCapabilityForPlatform imports are removed from capabilities.ts. platformDescriptors itself is kept (per the plan: "keep until proven redundant") — it remains the parity oracle that platform-plugin/__tests__/parity.test.ts pins the plugin buckets against.
  • capabilities.ts now calls registerBuiltinPlatformPlugins() at module load (idempotent, registers only lazy closures — no leaf imports, no cold-start cost). This is required for correctness: the admission path reaches isCommandSupportedOnDevice via daemon/handlers/response.ts without necessarily having loaded core/interactors.ts (the only other registrar), so the registry must be populated here too. The import chain (capabilities → register-builtins → {plugin, platform-inventory}) is value-cycle-free (those modules only type-import capabilities).

What I deliberately left hand-authored (b.2)

The per-command supports() / unsupportedHint() closures (src/core/command-descriptor/registry.ts:41-59isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator, supportsSynthesisGesture, supportsAndroidOrIosNonTv, synthesisGestureUnsupportedHint, plus the inline clipboard/alert/settings closures) stay VERBATIM on the command-descriptor facet.

They are per-command predicates; the plugin's capability.supportsByDefault is per-platform-family. Moving them onto the plugin would either lose the per-command shape (flatten — forbidden by perfect-shape §7) or require a per-command map on the plugin (breaking the §5.2 command-facet ownership for no benefit). There is no family-wide Apple guard hiding here — different Apple commands gate differently (isNotMacOs vs supportsAndroidOrIosNonTv vs supportsSynthesisGesture), so an apple-plugin supportsByDefault would be a no-op. This realizes the progress-plan's explicitly-sanctioned option 2: "keep them on the command facet and have the platform-level default flow through the plugin" (the bucket route is b.1; the closures stay put). The plugin's reserved supportsByDefault is left for the step-(c) AppleOS sub-platform guards.

Parity tests (the gate)

New src/core/__tests__/capability-plugin-routing-parity.test.ts — two independent oracles, proven byte-for-byte BEFORE the fold was removed:

  1. (b.1) bucket selection — for every real BASE_COMMAND_CAPABILITY_MATRIX shape (plus a synthetic web-bearing shape and a sparse shape) × every Platform, asserts capability[getPlugin(p).capability.bucket] deep-equals deriveCapabilityForPlatform(platformDescriptors, capability, p). deriveCapabilityForPlatform is retained as the before-derivation oracle, so a plugin-vs-descriptor disagreement fails the test (incl. the web bucket).
  2. (b.1) end-to-endisCommandSupportedOnDevice over the full {command × device} matrix (the reused device-fixtures.ts fixtures + an exhaustive synthetic platform×kind×target cross-product) equals an independent reference that reproduces the before pipeline (descriptor-fold bucket + verbatim supports closure + kind check) for all non-web platforms.
  3. (b.2) unsupportedHintunsupportedHintForDevice is byte-for-byte equal (string equality) to independent verbatim copies of the hint closures across the full matrix.
  4. (b.2) coverage guard — every command whose descriptor carries a supports/unsupportedHint closure is present in the reference map, so the oracle can't silently miss one.

web admission (not covered by the descriptor-fold reference, which lacks the web augmentation) is pinned by the bucket-selection test above plus the unchanged web column of capabilities.test.ts.

Byte-identical argument

The only change inside isCommandSupportedOnDevice is swapping the bucket-selection expression; the web augmentation, the supports gate, and the kind check are untouched. Test 1 proves the swapped expression is value-identical to the old fold for every capability shape × platform, and Tests 2-4 prove the full admission + hint surface is unchanged across the exhaustive device matrix. For a typed Platform, tryGetPlugin never returns undefined (registry totality is compile-asserted by BuiltinPluginsCoverAllPlatforms), so the return false fall-through is the unreachable counterpart of the old fold's never-default (perfect-shape §5.1).

Local verification (worktree)

  • tsc -p tsconfig.json
  • oxlint . --deny-warnings
  • oxfmt --check
  • rslib build
  • fallow audit --base origin/mainNo issues in changed files
  • vitest run --project unit: core (682), platforms/tests (706), the 5 capability/descriptor/plugin parity files (27), request-router-typed-error + args (137) — all green ✓

…ts() closures (Phase 3 step b)

b.1: isCommandSupportedOnDevice now reads each platform's capability bucket
from getPlugin(device.platform).capability.bucket (the PlatformPlugin registry,
ADR-0009) instead of the platformDescriptors fold. capabilities.ts registers the
builtin plugins at module load (idempotent, lazy closures only) so the admission
path populates the registry without depending on core/interactors.ts load order.

b.2: the per-command supports()/unsupportedHint() closures stay VERBATIM on the
command-descriptor facet; they cannot move to the plugin's per-FAMILY
capability.supportsByDefault without flattening their per-command shape
(perfect-shape §7). A new table-equivalence parity test pins both the bucket-route
swap and the closures byte-for-byte across the full platform x command x
device-kind x target matrix.
@github-actions

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.4 MB 1.4 MB -325 B
JS gzip 456.3 kB 456.3 kB -77 B
npm tarball 558.1 kB 558.0 kB -57 B
npm unpacked 2.0 MB 2.0 MB -325 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.2 ms 26.7 ms -0.4 ms
CLI --help 48.1 ms 47.0 ms -1.0 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/9722.js -325 B -77 B

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed against plans/perfect-shape.md, plans/phase3-platform-plugin-progress.md, ADR-0003, and ADR-0009. This is a scoped Phase 3 b.1/b.2 slice: capability bucket lookup now flows through PlatformPlugin, while the per-command supports and unsupportedHint closures remain on the command capability facet rather than being flattened into a family default. The parity test pins plugin bucket selection against the old platformDescriptors fold and separately pins the supports/hint behavior across the command/device matrix. I do not see a routing or architecture blocker; checks are green. 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 f430888 into main Jun 30, 2026
21 checks passed
@thymikee thymikee deleted the phase3-apple-step-b branch June 30, 2026 15:04
@github-actions

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