From 17e81c075f3aa88428cf1a98fce7abb06141df67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 27 Jun 2026 22:36:30 +0200 Subject: [PATCH 1/2] feat: derive capability bucket from a PlatformDescriptor registry Introduce src/core/platform-descriptor/ (mirroring src/core/command-descriptor/): a 5-row PlatformDescriptor registry carrying each leaf platform's capability bucket and isApple flag, pure derive folds, and a byte-for-byte parity test. Rewrite selectCapabilityForPlatform to fold the registry via the derive fn and delete the hand switch. Behaviorless and parity-proven; layering-safe (all in core, no utils->core inversion). Defers the ios/macos->apple collapse and the interactor/discovery/runner-profile tables to later per ADR-0009. --- src/core/capabilities.ts | 30 +++--- .../__tests__/parity.test.ts | 95 +++++++++++++++++++ src/core/platform-descriptor/derive.ts | 43 +++++++++ src/core/platform-descriptor/registry.ts | 40 ++++++++ src/core/platform-descriptor/types.ts | 34 +++++++ 5 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 src/core/platform-descriptor/__tests__/parity.test.ts create mode 100644 src/core/platform-descriptor/derive.ts create mode 100644 src/core/platform-descriptor/registry.ts create mode 100644 src/core/platform-descriptor/types.ts diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index 97bd07555..79b27a2d5 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -1,5 +1,7 @@ import { deriveCapabilityMatrix } from './command-descriptor/derive.ts'; import { commandDescriptors } from './command-descriptor/registry.ts'; +import { deriveCapabilityForPlatform } from './platform-descriptor/derive.ts'; +import { platformDescriptors } from './platform-descriptor/registry.ts'; import type { DeviceInfo } from '../utils/device.ts'; type KindMatrix = { @@ -67,29 +69,19 @@ function addWebCommandCapabilities( return result; } -// Exhaustive platform -> capability-bucket selection. Switching over the full Platform -// union (instead of an if/else ladder that funnels every unmatched platform into -// `capability.web`) makes adding a new Platform a compile error here, so a future -// platform can no longer silently inherit web's capability matrix. +// Platform -> capability-bucket selection, folded from the additive +// platform-descriptor registry (ADR-0009, Phase 3 step 1). The hand-authored +// switch was deleted after `platform-descriptor/__tests__/parity.test.ts` proved +// deriveCapabilityForPlatform is byte-equal to it across all five platforms. The +// registry's compile-time totality keeps the prior safety: adding a new Platform +// without a descriptor row is a compile error, so it can no longer silently +// inherit web's capability matrix. The registry only type-imports CommandCapability +// from here, so this value-level dependency does not form a runtime cycle. function selectCapabilityForPlatform( capability: CommandCapability, platform: DeviceInfo['platform'], ): KindMatrix | undefined { - switch (platform) { - case 'ios': - case 'macos': - return capability.apple; - case 'android': - return capability.android; - case 'linux': - return capability.linux; - case 'web': - return capability.web; - default: { - const exhaustive: never = platform; - return exhaustive; - } - } + return deriveCapabilityForPlatform(platformDescriptors, capability, platform); } export function isCommandSupportedOnDevice(command: string, device: DeviceInfo): boolean { diff --git a/src/core/platform-descriptor/__tests__/parity.test.ts b/src/core/platform-descriptor/__tests__/parity.test.ts new file mode 100644 index 000000000..fd22948cf --- /dev/null +++ b/src/core/platform-descriptor/__tests__/parity.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { isApplePlatform, PLATFORMS, type Platform } from '../../../utils/device.ts'; +import type { CommandCapability } from '../../capabilities.ts'; +import { deriveApplePlatforms, deriveCapabilityForPlatform } from '../derive.ts'; +import { platformDescriptors } from '../registry.ts'; +import type { CapabilityBucket } from '../types.ts'; + +// Independent VERBATIM copy of the hand-authored `selectCapabilityForPlatform` +// switch (src/core/capabilities.ts, before this slice deleted it). The derive +// fold is proven byte-for-byte equal to THIS reference — not to the production +// function — so the assertion stays meaningful after the flip wires +// `selectCapabilityForPlatform` onto the derive (which would make a +// derived-vs-production comparison a tautology). +function selectCapabilityByHandSwitch( + capability: CommandCapability, + platform: Platform, +): CommandCapability[CapabilityBucket] { + switch (platform) { + case 'ios': + case 'macos': + return capability.apple; + case 'android': + return capability.android; + case 'linux': + return capability.linux; + case 'web': + return capability.web; + default: { + const exhaustive: never = platform; + return exhaustive; + } + } +} + +// Distinct object identities per bucket so a wrong-bucket selection fails the +// `===` (reference) check below, plus a sparse capability to prove `undefined` +// propagation when a family is absent. +const DENSE_CAPABILITY: CommandCapability = { + apple: { simulator: true, device: true }, + android: { emulator: true, device: true, unknown: true }, + linux: { device: true }, + web: { device: true }, +}; +const SPARSE_CAPABILITY: CommandCapability = { + apple: { simulator: true }, +}; + +test('derived bucket selection is value-identical to the hand switch for every platform', () => { + for (const capability of [DENSE_CAPABILITY, SPARSE_CAPABILITY]) { + for (const platform of PLATFORMS) { + assert.equal( + deriveCapabilityForPlatform(platformDescriptors, capability, platform), + selectCapabilityByHandSwitch(capability, platform), + `bucket selection for ${platform}`, + ); + } + } +}); + +test('descriptor Apple rows equal the leaf platforms where isApplePlatform is true', () => { + const fromDescriptors = deriveApplePlatforms(platformDescriptors); + const fromPredicate = PLATFORMS.filter((platform) => isApplePlatform(platform)); + + assert.deepEqual( + [...fromDescriptors].sort(), + [...fromPredicate].sort(), + 'apple leaf platform set', + ); + + // The descriptor filter and the standalone fold agree. + assert.deepEqual( + fromDescriptors, + platformDescriptors + .filter((descriptor) => descriptor.isApple) + .map((descriptor) => descriptor.platform), + ); + + // isApple is exactly the `apple` capability bucket for every row — no third state. + for (const descriptor of platformDescriptors) { + assert.equal( + descriptor.isApple, + descriptor.capabilityBucket === 'apple', + `${descriptor.platform} isApple matches apple bucket`, + ); + } +}); + +test('registry covers every leaf platform in PLATFORMS order (totality)', () => { + assert.deepEqual( + platformDescriptors.map((descriptor) => descriptor.platform), + [...PLATFORMS], + 'descriptor platforms equal PLATFORMS in order', + ); +}); diff --git a/src/core/platform-descriptor/derive.ts b/src/core/platform-descriptor/derive.ts new file mode 100644 index 000000000..9ae053e90 --- /dev/null +++ b/src/core/platform-descriptor/derive.ts @@ -0,0 +1,43 @@ +import type { Platform } from '../../utils/device.ts'; +import type { CommandCapability } from '../capabilities.ts'; +import type { CapabilityBucket, PlatformDescriptor } from './types.ts'; + +/** + * Pure folds over the additive {@link PlatformDescriptor} registry (ADR-0009, + * Phase 3 step 1). These reproduce facts that today live in hand-written control + * flow so the parity test can prove byte-for-byte equality before the hand + * switch is deleted. + * + * This module only TYPE-imports from {@link CommandCapability} (erased at runtime + * under `verbatimModuleSyntax`) and from `utils/device.ts`, so wiring it into + * `capabilities.ts` forms no runtime cycle — mirroring `command-descriptor/derive.ts`. + */ + +/** + * Reproduces `selectCapabilityForPlatform`'s bucket selection EXACTLY: the leaf + * `platform` is mapped to its `capabilityBucket` via the registry, then that + * family is read off the `capability` (returning `undefined` when the family is + * absent, identical to the hand switch). + * + * The registry's compile-time totality (`PlatformDescriptorsAreTotal`) plus the + * parity test's order-equality assertion guarantee `find` always resolves; the + * throw is the unreachable counterpart of the hand switch's `never` default. + */ +export function deriveCapabilityForPlatform( + descriptors: readonly PlatformDescriptor[], + capability: CommandCapability, + platform: Platform, +): CommandCapability[CapabilityBucket] { + const descriptor = descriptors.find((entry) => entry.platform === platform); + if (!descriptor) { + throw new Error(`No PlatformDescriptor registered for platform "${platform}"`); + } + return capability[descriptor.capabilityBucket]; +} + +/** Reproduces the set of leaf platforms for which `isApplePlatform` is true. */ +export function deriveApplePlatforms(descriptors: readonly PlatformDescriptor[]): Platform[] { + return descriptors + .filter((descriptor) => descriptor.isApple) + .map((descriptor) => descriptor.platform); +} diff --git a/src/core/platform-descriptor/registry.ts b/src/core/platform-descriptor/registry.ts new file mode 100644 index 000000000..f01b59790 --- /dev/null +++ b/src/core/platform-descriptor/registry.ts @@ -0,0 +1,40 @@ +import type { Platform } from '../../utils/device.ts'; +import type { PlatformDescriptor } from './types.ts'; + +/** + * The additive single source of truth for the platform→capability-bucket fan-out + * and the Apple-platform predicate (ADR-0009, Phase 3 step 1). + * + * Each row is copied VERBATIM from the facts the hand-authored control flow + * implies today: + * - `capabilityBucket` — the bucket `selectCapabilityForPlatform` returned for + * the platform (`ios`/`macos`→`apple`, `android`→`android`, + * `linux`→`linux`, `web`→`web`). + * - `isApple` — whether `isApplePlatform` is true for the leaf platform + * (`ios`/`macos` only). + * + * `as const satisfies` pins each literal while checking the shape, and the row + * order matches the `PLATFORMS` tuple so the parity test can prove totality. + */ +export const platformDescriptors = [ + { platform: 'ios', capabilityBucket: 'apple', isApple: true }, + { platform: 'macos', capabilityBucket: 'apple', isApple: true }, + { platform: 'android', capabilityBucket: 'android', isApple: false }, + { platform: 'linux', capabilityBucket: 'linux', isApple: false }, + { platform: 'web', capabilityBucket: 'web', isApple: false }, +] as const satisfies readonly PlatformDescriptor[]; + +/** The union of leaf platforms that carry a descriptor row. */ +type CoveredPlatform = (typeof platformDescriptors)[number]['platform']; + +/** + * Compile-time totality, mirroring the exhaustive `never` of the hand switch this + * registry replaces: if a new leaf platform is added to `PLATFORMS` without a row + * here, `Platform` no longer extends `CoveredPlatform` and this alias resolves to + * `false`, which violates the `extends true` constraint and fails the build. The + * value-level coverage (same order) is asserted by the parity test. + */ +type AssertTrue = T; +export type PlatformDescriptorsAreTotal = AssertTrue< + [Platform] extends [CoveredPlatform] ? true : false +>; diff --git a/src/core/platform-descriptor/types.ts b/src/core/platform-descriptor/types.ts new file mode 100644 index 000000000..f91b133ab --- /dev/null +++ b/src/core/platform-descriptor/types.ts @@ -0,0 +1,34 @@ +import type { Platform } from '../../utils/device.ts'; + +/** + * The capability-bucket key a leaf {@link Platform} reads from a + * {@link CommandCapability}. These are exactly the per-family keys of + * `CommandCapability` (`apple` / `android` / `linux` / `web`) — the buckets the + * hand-authored `selectCapabilityForPlatform` switch fanned each platform into. + */ +export type CapabilityBucket = 'apple' | 'android' | 'linux' | 'web'; + +/** + * The single additive platform-descriptor shape (ADR-0009, Phase 3 step 1). + * + * Per leaf platform this carries, side-by-side, the two facts that today are + * implied by hand-written control flow: + * - `capabilityBucket` — which `CommandCapability` family the platform reads + * (from the `selectCapabilityForPlatform` switch). + * - `isApple` — whether the platform is an Apple platform + * (mirrors `isApplePlatform` for leaf platforms). + * + * `Platform` stays sourced from `utils/device.ts`; the registry only + * `satisfies`-checks against it (it does not become its source), which keeps the + * utils→core layering one-directional and avoids an import cycle. + */ +export type PlatformDescriptor = { + platform: Platform; + capabilityBucket: CapabilityBucket; + isApple: boolean; +}; + +/** Identity helper that pins each entry to the {@link PlatformDescriptor} shape. */ +export function definePlatformDescriptor(descriptor: PlatformDescriptor): PlatformDescriptor { + return descriptor; +} From 2f5ffb2b8415e335213e6f41c8a874c7cf2c261e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 27 Jun 2026 23:20:45 +0200 Subject: [PATCH 2/2] refactor: drop unused definePlatformDescriptor helper The identity helper had no consumers (the registry uses `as const satisfies`), so Fallow flagged it as a newly-added unused export. Remove it. --- src/core/platform-descriptor/types.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/platform-descriptor/types.ts b/src/core/platform-descriptor/types.ts index f91b133ab..d3b30b18f 100644 --- a/src/core/platform-descriptor/types.ts +++ b/src/core/platform-descriptor/types.ts @@ -27,8 +27,3 @@ export type PlatformDescriptor = { capabilityBucket: CapabilityBucket; isApple: boolean; }; - -/** Identity helper that pins each entry to the {@link PlatformDescriptor} shape. */ -export function definePlatformDescriptor(descriptor: PlatformDescriptor): PlatformDescriptor { - return descriptor; -}