Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions src/core/capabilities.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down
95 changes: 95 additions & 0 deletions src/core/platform-descriptor/__tests__/parity.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
43 changes: 43 additions & 0 deletions src/core/platform-descriptor/derive.ts
Original file line number Diff line number Diff line change
@@ -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);
}
40 changes: 40 additions & 0 deletions src/core/platform-descriptor/registry.ts
Original file line number Diff line number Diff line change
@@ -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 extends true> = T;
export type PlatformDescriptorsAreTotal = AssertTrue<
[Platform] extends [CoveredPlatform] ? true : false
>;
29 changes: 29 additions & 0 deletions src/core/platform-descriptor/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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;
};
Loading