From 0fd2f9555e8a37de1acbd085a90dc5f76649c11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 1 Jul 2026 11:30:50 +0200 Subject: [PATCH] refactor: relocate Apple supports()/unsupportedHint() closures onto the plugin (#973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 step b.2. Move the per-command supports() / unsupportedHint() device closures VERBATIM off the command-descriptor facet onto the owning PlatformPlugin's capability.supportsByDefault / unsupportedHintByDefault (perfect-shape §7 / ADR-0009: relocate, never flatten). Bodies are byte-for-byte identical; only their ownership moves to the Apple plugin, the family that owns every discriminating device (macOS-coordinate-pinch, tvOS-no-touch, physical-iOS, two-finger-synthesis). The relocation is faithful because every closure is a no-op (returns true / undefined) on non-Apple devices, so consulting it only for the Apple family leaves admission unchanged across the full device matrix. isCommandSupportedOnDevice and unsupportedHintForDevice now read the closure off getPlugin(device.platform); the command facet carries platform/kind buckets only, and supports/unsupportedHint are removed from the CommandCapability type. Parity gate (byte-for-byte, before deleting the hand sites): independent VERBATIM oracle in capability-plugin-routing-parity.test.ts pins (a) production admission + hint output unchanged across the {platform x command x kind x target} matrix, and (b) the relocated Apple-plugin closures are behaviorally identical to the originals across the device-fixtures sample matrix, with a guard that no non-Apple family grew a gate. --- .../capability-plugin-routing-parity.test.ts | 81 +++++++++++++++---- src/core/capabilities.ts | 25 ++++-- .../__tests__/parity.test.ts | 8 +- src/core/command-descriptor/registry.ts | 50 ++---------- src/core/platform-plugin/plugin.ts | 20 ++++- src/core/platform-plugin/register-builtins.ts | 73 ++++++++++++++++- 6 files changed, 180 insertions(+), 77 deletions(-) diff --git a/src/core/__tests__/capability-plugin-routing-parity.test.ts b/src/core/__tests__/capability-plugin-routing-parity.test.ts index 977742dec..9ebeb8f4f 100644 --- a/src/core/__tests__/capability-plugin-routing-parity.test.ts +++ b/src/core/__tests__/capability-plugin-routing-parity.test.ts @@ -29,19 +29,25 @@ import { platformDescriptors } from '../platform-descriptor/registry.ts'; import { getPlugin } from '../platform-plugin/plugin.ts'; import { registerBuiltinPlatformPlugins } from '../platform-plugin/register-builtins.ts'; -// Phase 3 step (b) parity gate. Two independent oracles pin that the migration is +// Phase 3 step (b) parity gate. Independent oracles pin that the migration is // byte-for-byte behaviorless: // (b.1) the platform -> capability-bucket selection in `isCommandSupportedOnDevice` // now flows through the PlatformPlugin registry instead of the // `platformDescriptors` fold. `deriveCapabilityForPlatform(platformDescriptors, // ...)` is kept here as the BEFORE-derivation oracle (the production fold was // deleted), so a plugin-vs-descriptor disagreement fails this test. -// (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). Independent verbatim copies below pin that the closures, -// as they flow through `deriveCapabilityMatrix` into admission, are unchanged -// across the full {platform x command x device-kind x target} matrix. +// (b.2) the per-command `supports()` / `unsupportedHint()` device closures were +// RELOCATED VERBATIM off the command-descriptor facet onto the owning +// PlatformPlugin's `capability.supportsByDefault` / `unsupportedHintByDefault` +// (perfect-shape §7: relocate, never flatten). This is faithful because every +// such closure is a no-op (returns `true` / `undefined`) on non-Apple devices, +// so consulting it only for the Apple family — the family that owns the +// relocated map — leaves admission unchanged. The independent VERBATIM copies +// below are the oracle: they pin (a) that production admission (`isCommand +// SupportedOnDevice`) and hint output (`unsupportedHintForDevice`) are unchanged +// across the full {platform x command x device-kind x target} matrix, and (b) +// that the closures now living on the Apple plugin are byte-for-byte behaviorally +// identical to the originals across the sample-device matrix. registerBuiltinPlatformPlugins(); @@ -211,17 +217,58 @@ test('(b.2) unsupportedHint closures are verbatim across the full device matrix' } }); -test('(b.2) every command carrying a supports closure is covered by the reference map', () => { - // Guards the SUPPORTS_REF/HINT_REF oracle against silently missing a closure: a - // command whose admission depends on a supports gate must appear in SUPPORTS_REF, - // and every hint-bearing command must appear in HINT_REF. - for (const command of listCapabilityCommands()) { - const capability = BASE_COMMAND_CAPABILITY_MATRIX[command]; - if (capability?.supports) { - assert.ok(SUPPORTS_REF[command], `${command} supports closure present in reference map`); +test('(b.2) the Apple plugin carries exactly the relocated supports/hint closures', () => { + // The relocation target: `supports()` / `unsupportedHint()` now live on the Apple + // plugin (the family that owns every discriminating device). Pin the RELOCATED maps' + // key sets against the independent verbatim reference so no closure was silently + // dropped or added while moving off the command facet. + const appleCapability = getPlugin('ios').capability; + assert.deepEqual( + Object.keys(appleCapability.supportsByDefault ?? {}).sort(), + Object.keys(SUPPORTS_REF).sort(), + 'supportsByDefault key set equals the verbatim reference', + ); + assert.deepEqual( + Object.keys(appleCapability.unsupportedHintByDefault ?? {}).sort(), + Object.keys(HINT_REF).sort(), + 'unsupportedHintByDefault key set equals the verbatim reference', + ); + // ios and macos are the SAME Apple plugin instance, so both leaves read one map. + assert.equal(getPlugin('ios').capability, getPlugin('macos').capability); +}); + +test('(b.2) the relocated Apple closures are byte-for-byte the verbatim originals', () => { + // Closure-equivalence: for every command x sample-device, the closure now living on + // the Apple plugin returns an identical boolean / identical hint STRING to the + // independent verbatim copy of the original command-facet closure. + const appleCapability = getPlugin('ios').capability; + for (const [command, reference] of Object.entries(SUPPORTS_REF)) { + const relocated = appleCapability.supportsByDefault?.[command]; + assert.ok(relocated, `${command} supports closure present on the Apple plugin`); + for (const device of SAMPLE_DEVICES) { + assert.equal(relocated(device), reference(device), `${command} supports on ${device.id}`); } - if (capability?.unsupportedHint) { - assert.ok(HINT_REF[command], `${command} unsupportedHint closure present in reference map`); + } + for (const [command, reference] of Object.entries(HINT_REF)) { + const relocated = appleCapability.unsupportedHintByDefault?.[command]; + assert.ok(relocated, `${command} hint closure present on the Apple plugin`); + for (const device of SAMPLE_DEVICES) { + assert.equal(relocated(device), reference(device), `${command} hint on ${device.id}`); } } }); + +test('(b.2) no non-Apple family carries a relocated supports/hint closure', () => { + // The relocation is faithful only because the closures are no-ops off the Apple + // family; guard that no other plugin accidentally grew a per-command gate (which + // would change admission for android/linux/web). + for (const platform of ['android', 'linux', 'web'] as const) { + const capability = getPlugin(platform).capability; + assert.equal(capability.supportsByDefault, undefined, `${platform} has no supportsByDefault`); + assert.equal( + capability.unsupportedHintByDefault, + undefined, + `${platform} has no unsupportedHintByDefault`, + ); + } +}); diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index d7cc15d49..fd8c2cc49 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -24,9 +24,6 @@ export type CommandCapability = { android?: KindMatrix; linux?: KindMatrix; web?: KindMatrix; - supports?: (device: DeviceInfo) => boolean; - /** Optional actionable hint surfaced when this command is rejected at admission for `device`. */ - unsupportedHint?: (device: DeviceInfo) => string | undefined; }; const WEB_DEVICE: KindMatrix = { device: true }; @@ -52,9 +49,12 @@ const WEB_SUPPORTED_COMMANDS = new Set([ ]); // Built from the additive command-descriptor registry (ADR-0008, Phase 1 step 3). // The hand-authored literal was deleted after #906 proved deriveCapabilityMatrix is -// byte-equal to it (platform/kind buckets plus the supports/unsupportedHint closures, -// across the sample-device matrix). The registry only type-imports CommandCapability -// from here, so this value-level dependency does not form a runtime cycle. +// byte-equal to it (platform/kind buckets). The per-command `supports()` / +// `unsupportedHint()` device closures no longer live here — they were relocated onto +// the owning PlatformPlugin's `capability.supportsByDefault` / `unsupportedHintByDefault` +// in Phase 3 step b.2 (see `isCommandSupportedOnDevice` below). The registry only +// type-imports CommandCapability from here, so this value-level dependency does not +// form a runtime cycle. export const BASE_COMMAND_CAPABILITY_MATRIX: Record = deriveCapabilityMatrix(commandDescriptors); @@ -95,13 +95,22 @@ export function isCommandSupportedOnDevice(command: string, device: DeviceInfo): if (!plugin) return false; const byPlatform = capability[plugin.capability.bucket]; if (!byPlatform) return false; - if (capability.supports && !capability.supports(device)) return false; + // The per-command `supports()` gate now flows through the owning PlatformPlugin + // (ADR-0009, Phase 3 step b.2): the family that owns `device.platform` carries the + // `supports()` closure RELOCATED VERBATIM in `capability.supportsByDefault`, keyed by + // command. A family with no entry for `command` admits it unchanged — proven equal to + // the former command-facet closure across the device matrix by + // `__tests__/capability-plugin-routing-parity.test.ts`. + const supportsByDefault = plugin.capability.supportsByDefault?.[command]; + if (supportsByDefault && !supportsByDefault(device)) return false; const kind = (device.kind ?? 'unknown') as keyof KindMatrix; return byPlatform[kind] === true; } export function unsupportedHintForDevice(command: string, device: DeviceInfo): string | undefined { - return COMMAND_CAPABILITY_MATRIX[command]?.unsupportedHint?.(device); + // Counterpart of the `supports()` relocation (Phase 3 step b.2): the hint closure is + // owned by the family that owns `device.platform`, keyed by command. + return tryGetPlugin(device.platform)?.capability.unsupportedHintByDefault?.[command]?.(device); } export function listCapabilityCommands(): string[] { diff --git a/src/core/command-descriptor/__tests__/parity.test.ts b/src/core/command-descriptor/__tests__/parity.test.ts index cf05288d1..1a5fe5ab9 100644 --- a/src/core/command-descriptor/__tests__/parity.test.ts +++ b/src/core/command-descriptor/__tests__/parity.test.ts @@ -116,10 +116,10 @@ test('capability matrix holds its admission invariants', () => { const hasPlatformBucket = Boolean( capability.apple || capability.android || capability.linux || capability.web, ); - assert.ok( - hasPlatformBucket || typeof capability.supports === 'function', - `${command} has a platform bucket or a supports predicate`, - ); + // Every capability entry is now selectable purely by its platform buckets: the + // per-command `supports()` gate was relocated onto the owning PlatformPlugin + // (`capability.supportsByDefault`) in Phase 3 step b.2, so it no longer lives here. + assert.ok(hasPlatformBucket, `${command} has a platform bucket`); } const covered = new Set(Object.keys(BASE_COMMAND_CAPABILITY_MATRIX)); diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 5fba34cdf..8727fd6aa 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -5,7 +5,6 @@ import { } from '../../command-catalog.ts'; import type { CommandCapability } from '../capabilities.ts'; import type { DaemonRequest } from '../../daemon/types.ts'; -import { isTvOsDevice, type DeviceInfo } from '../../kernel/device.ts'; import type { CommandDescriptor } from './types.ts'; // --------------------------------------------------------------------------- @@ -34,30 +33,16 @@ const isShardedTestRequest = (req: DaemonRequest): boolean => (typeof req.flags?.shardAll === 'number' || typeof req.flags?.shardSplit === 'number'); // --------------------------------------------------------------------------- -// Capability predicates / matrices — copied VERBATIM from +// Capability matrices — platform/kind buckets, copied VERBATIM from // src/core/capabilities.ts (BASE_COMMAND_CAPABILITY_MATRIX). +// +// The per-command `supports()` / `unsupportedHint()` device closures that used to +// live here were RELOCATED VERBATIM onto the owning PlatformPlugin's +// `capability.supportsByDefault` / `unsupportedHintByDefault` in Phase 3 step b.2 +// (src/core/platform-plugin/register-builtins.ts). The capability facet now carries +// platform/kind buckets only; admission reads the closure off the plugin. // --------------------------------------------------------------------------- -const isNotMacOs = (device: DeviceInfo): boolean => device.platform !== 'macos'; -const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean => - device.platform === 'macos' || device.kind === 'simulator'; -const isIosMobileSimulator = (device: DeviceInfo): boolean => - device.platform === 'ios' && device.kind === 'simulator' && !isTvOsDevice(device); -const supportsSynthesisGesture = (device: DeviceInfo): boolean => - device.platform === 'android' || isIosMobileSimulator(device); -const supportsAndroidOrIosNonTv = (device: DeviceInfo): boolean => - device.platform === 'android' || (device.platform === 'ios' && !isTvOsDevice(device)); - -const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined => { - if (device.platform === 'macos') - return 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.'; - if (isTvOsDevice(device)) - return 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.'; - if (device.platform === 'ios' && device.kind === 'device') - return 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.'; - return undefined; -}; - const APPLE_SIM_AND_DEVICE = { simulator: true, device: true }; const ANDROID_ALL = { emulator: true, device: true, unknown: true }; const LINUX_DEVICE = { device: true }; @@ -78,7 +63,6 @@ const APP_INSTALL_CAPABILITY = { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: isNotMacOs, } satisfies CommandCapability; // --------------------------------------------------------------------------- @@ -142,7 +126,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: isNotMacOs, }, batchable: true, }, @@ -209,11 +192,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE, - supports: (device) => - device.platform === 'android' || - device.platform === 'linux' || - device.platform === 'macos' || - device.kind === 'simulator', }, batchable: true, }, @@ -224,7 +202,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: supportsAndroidOrIosNonTv, }, batchable: true, }, @@ -257,7 +234,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: { simulator: true }, android: ANDROID_ALL, linux: LINUX_NONE, - supports: isNotMacOs, }, batchable: true, }, @@ -316,7 +292,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: (device) => device.platform === 'android' || isMacOsOrAppleSimulator(device), }, batchable: true, }, @@ -327,8 +302,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: (device) => - device.platform === 'android' || device.platform === 'macos' || device.kind === 'simulator', }, batchable: true, }, @@ -426,7 +399,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE, - supports: isNotMacOs, }, batchable: true, }, @@ -437,7 +409,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: supportsAndroidOrIosNonTv, }, batchable: true, }, @@ -460,8 +431,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: supportsSynthesisGesture, - unsupportedHint: synthesisGestureUnsupportedHint, }, batchable: false, }, @@ -502,8 +471,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: supportsSynthesisGesture, - unsupportedHint: synthesisGestureUnsupportedHint, }, batchable: false, }, @@ -514,8 +481,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: supportsSynthesisGesture, - unsupportedHint: synthesisGestureUnsupportedHint, }, batchable: false, }, @@ -527,7 +492,6 @@ const RAW_COMMAND_DESCRIPTORS = [ apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE, - supports: isNotMacOs, }, batchable: true, }, diff --git a/src/core/platform-plugin/plugin.ts b/src/core/platform-plugin/plugin.ts index b99e12912..a0864c345 100644 --- a/src/core/platform-plugin/plugin.ts +++ b/src/core/platform-plugin/plugin.ts @@ -43,13 +43,25 @@ export type PlatformPlugin = { /** * The capability facet. `bucket` is the {@link CapabilityBucket} this family * reads from a `CommandCapability` (parity-checked against the existing - * `platformDescriptors` registry). `supportsByDefault` is reserved for the - * step-(b) relocation of the `supports()` device closures — left undefined - * here so those closures stay verbatim in `capabilities.ts` for now. + * `platformDescriptors` registry). + * + * `supportsByDefault` / `unsupportedHintByDefault` carry the per-command + * `supports()` / `unsupportedHint()` device closures RELOCATED VERBATIM off the + * command-descriptor facet (ADR-0009 / perfect-shape §7 step b.2: relocate, never + * flatten). They are keyed by command name and owned by the family that owns the + * device's platform; `isCommandSupportedOnDevice` / `unsupportedHintForDevice` + * consult the map for `getPlugin(device.platform)`, so a family with no entry for a + * command (the key is absent) admits it unchanged. Only the Apple family carries + * entries today — every relocated closure is a no-op (returns `true` / `undefined`) + * on non-Apple devices, proven byte-for-byte by the parity gate before the + * command-facet closures were deleted. */ readonly capability: { readonly bucket: CapabilityBucket; - supportsByDefault?(device: DeviceInfo): boolean; + readonly supportsByDefault?: Readonly boolean>>; + readonly unsupportedHintByDefault?: Readonly< + Record string | undefined> + >; }; /** * The daemon app-log facet (issue #974). `resolveBackend` wraps the platform diff --git a/src/core/platform-plugin/register-builtins.ts b/src/core/platform-plugin/register-builtins.ts index d42afe60b..ddcb10d5a 100644 --- a/src/core/platform-plugin/register-builtins.ts +++ b/src/core/platform-plugin/register-builtins.ts @@ -1,9 +1,76 @@ import { registerPlatformPlugin, type PlatformPlugin } from './plugin.ts'; +import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; import { shouldUseHostMacFastPath, WEB_DESKTOP_DEVICE } from '../platform-inventory.ts'; import type { Platform, DeviceInfo } from '../../kernel/device.ts'; import type { DeviceInventoryRequest } from '../platform-inventory.ts'; import type { RunnerContext } from '../interactor-types.ts'; +// --------------------------------------------------------------------------- +// Apple family per-command capability closures — RELOCATED VERBATIM from +// src/core/command-descriptor/registry.ts (ADR-0009 / perfect-shape §7 step b.2: +// relocate, never flatten). Every body below is byte-for-byte the former +// command-facet `supports()` / `unsupportedHint()` closure; the parity gate +// (src/core/__tests__/capability-plugin-routing-parity.test.ts) pins them +// behaviorally against an INDEPENDENT verbatim oracle across the full device +// matrix. Each closure is a no-op (returns `true` / `undefined`) on non-Apple +// devices, so consulting them only for the Apple family leaves admission unchanged. +// --------------------------------------------------------------------------- + +const isNotMacOs = (device: DeviceInfo): boolean => device.platform !== 'macos'; +const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean => + device.platform === 'macos' || device.kind === 'simulator'; +const isIosMobileSimulator = (device: DeviceInfo): boolean => + device.platform === 'ios' && device.kind === 'simulator' && device.target !== 'tv'; +const supportsSynthesisGesture = (device: DeviceInfo): boolean => + device.platform === 'android' || isIosMobileSimulator(device); +const supportsAndroidOrIosNonTv = (device: DeviceInfo): boolean => + device.platform === 'android' || (device.platform === 'ios' && device.target !== 'tv'); + +const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined => { + if (device.platform === 'macos') + return 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.'; + if (device.platform === 'ios' && device.target === 'tv') + return 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.'; + if (device.platform === 'ios' && device.kind === 'device') + return 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.'; + return undefined; +}; + +// Per-command support gates the Apple family applies by default, keyed exactly as in +// the command-descriptor registry (a command absent here has no Apple gate). +const APPLE_SUPPORTS_BY_DEFAULT: Record boolean> = { + [PUBLIC_COMMANDS.boot]: isNotMacOs, + [PUBLIC_COMMANDS.install]: isNotMacOs, + [PUBLIC_COMMANDS.reinstall]: isNotMacOs, + [PUBLIC_COMMANDS.installFromSource]: isNotMacOs, + [PUBLIC_COMMANDS.push]: isNotMacOs, + [PUBLIC_COMMANDS.home]: isNotMacOs, + [PUBLIC_COMMANDS.appSwitcher]: isNotMacOs, + [PUBLIC_COMMANDS.clipboard]: (device) => + device.platform === 'android' || + device.platform === 'linux' || + device.platform === 'macos' || + device.kind === 'simulator', + [PUBLIC_COMMANDS.keyboard]: supportsAndroidOrIosNonTv, + [PUBLIC_COMMANDS.rotate]: supportsAndroidOrIosNonTv, + [PUBLIC_COMMANDS.alert]: (device) => + device.platform === 'android' || isMacOsOrAppleSimulator(device), + [PUBLIC_COMMANDS.settings]: (device) => + device.platform === 'android' || device.platform === 'macos' || device.kind === 'simulator', + pinch: supportsSynthesisGesture, + 'rotate-gesture': supportsSynthesisGesture, + 'transform-gesture': supportsSynthesisGesture, +}; + +const APPLE_UNSUPPORTED_HINT_BY_DEFAULT: Record< + string, + (device: DeviceInfo) => string | undefined +> = { + pinch: synthesisGestureUnsupportedHint, + 'rotate-gesture': synthesisGestureUnsupportedHint, + 'transform-gesture': synthesisGestureUnsupportedHint, +}; + // Each plugin WRAPS today's existing factories (src/core/interactors/*) and the // inventory if-chain (src/core/platform-inventory.ts) as LAZY methods. No leaf // code is rewritten: the dynamic `import()`s and the per-platform list calls are @@ -17,7 +84,11 @@ const applePlugin = { // Apple owns BOTH leaf platforms today — mirrors `case 'ios': case 'macos':`. platforms: ['ios', 'macos'], familySelector: 'apple', - capability: { bucket: 'apple' }, + capability: { + bucket: 'apple', + supportsByDefault: APPLE_SUPPORTS_BY_DEFAULT, + unsupportedHintByDefault: APPLE_UNSUPPORTED_HINT_BY_DEFAULT, + }, // Wraps the Apple arm of `resolveLogBackend` verbatim: macOS -> 'macos'; // an iOS `device` -> 'ios-device'; every other iOS kind -> 'ios-simulator'. appLog: {