feat: PlatformDescriptor registry deriving the capability bucket — Phase 3 step 1#914
Conversation
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.
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed — behaviorless and parity-proven. |
The identity helper had no consumers (the registry uses `as const satisfies`), so Fallow flagged it as a newly-added unused export. Remove it.
|
Reviewed this against What I checked:
Residual risk is low and mostly architectural: this is still an additive registry foothold, not the full PlatformPlugin migration. I would merge after maintainers are happy with the new |
|
What
Phase 3 step 1 of the perfect-shape roadmap (ADR-0009, PlatformPlugin). Introduces
src/core/platform-descriptor/— mirroring exactly how Phase 1's command-descriptorregistry started — and derives the platform→capability-bucket fan-out from it behind
a byte-for-byte parity test.
New:
src/core/platform-descriptor/types.ts—CapabilityBucket = 'apple' | 'android' | 'linux' | 'web'andPlatformDescriptor = { platform: Platform; capabilityBucket: CapabilityBucket; isApple: boolean }.Platformstays sourced fromutils/device.ts; the registry onlysatisfies-checksagainst it (it does not become its source), avoiding a utils↔core import cycle.
registry.ts— a 5-rowas const satisfies readonly PlatformDescriptor[](
ios/macos→apple,android→android,linux→linux,web→web), inPLATFORMSorder. A compile-time totality alias preserves the priorneversafety: adding a new
Platformwithout a row fails the build.derive.ts— pure folds:deriveCapabilityForPlatform(descriptors, capability, platform)reproduces the hand switch exactly (maps platform→bucket via the registry, reads that
family off the capability), and
deriveApplePlatforms(descriptors). Imports onlytype-level
CommandCapabilityfrom../capabilities.tsandPlatformfrom../../utils/device.ts— nothing from commands/daemon/platforms.__tests__/parity.test.ts— proves (a) the derive is value-identical to anindependent VERBATIM copy of the old switch for every platform in
PLATFORMS(dense + sparse capabilities, reference-identity comparison), (b) the descriptor
Apple rows equal the leaf platforms where
isApplePlatform()is true andisApple === (capabilityBucket === 'apple')for every row, (c) totality:platformDescriptors.map(d => d.platform)deep-equals[...PLATFORMS]in order.The one flip (behaviorless, parity-proven)
selectCapabilityForPlatforminsrc/core/capabilities.tsnow folds the registry viaderiveCapabilityForPlatform; the handswitchis deleted. Signature + behavior areidentical — the consumer suite (
core/__tests__/capabilities.test.ts, exercising allfive platforms through
isCommandSupportedOnDevice) stays green unchanged.Layering
All new code lives in
src/core. Thederive.tsonly type-imports fromcapabilities.ts(erased under
verbatimModuleSyntax), so wiring it intocapabilities.tsforms no runtimecycle — same pattern as
command-descriptor/derive.ts. The CI Layering Guard(
src/daemon&src/platformsmust not importcommands/) is unaffected.Deliberately deferred (per ADR-0009)
isApplePlatforminutils/device.ts(re-expressing it via the coreregistry would invert utils→core layering).
ios+macos→applecollapse.(
platform-inventory.ts), the Apple runner-profileRecord, or thesupports()/unsupportedHint()closures.Verification
tsc -p tsconfig.json --noEmit— exit 0oxfmt --write+oxlint --deny-warningson changed files — exit 0vitest runforcore/platform-descriptor,core/capabilities,command-descriptor,and the
utils/argsconsumer — all green