Skip to content

feat: command-descriptor registry (additive, parity-tested) — Phase 1 step 1#906

Merged
thymikee merged 1 commit into
mainfrom
feat/command-descriptor-registry
Jun 27, 2026
Merged

feat: command-descriptor registry (additive, parity-tested) — Phase 1 step 1#906
thymikee merged 1 commit into
mainfrom
feat/command-descriptor-registry

Conversation

@thymikee

Copy link
Copy Markdown
Member

What

ADR-0008 Phase 1 step 1: introduce a single CommandDescriptor registry as the strangler-fig foundation for the command-descriptor migration. This is purely additive and dormant — nothing reads the new registry yet; it exists only to be proven byte-equal to today's hand tables.

New files (src/commands/descriptor/)

  • types.tsCommandDescriptor type + defineCommandDescriptor(). Per command it carries the daemon route + request-policy traits (reusing the existing DaemonCommandDescriptor shape including the closure traits allowSessionlessDefaultDevice / skipSessionlessProviderDevice, not flattened), an optional CommandCapability entry, and batchable / mcpExposed flags.
  • registry.tscommandDescriptors, the additive single source. Covers the full daemon key space (public + internal + gesture-alias names) plus the two capability/batch-only commands (app-switcher, install-from-source). Daemon traits, capability, and batchable are copied verbatim from DAEMON_COMMAND_DESCRIPTORS, BASE_COMMAND_CAPABILITY_MATRIX, and STRUCTURED_BATCH_COMMAND_NAMES.
  • derive.ts — pure folds: deriveDaemonCommandDescriptors, deriveCapabilityMatrix, deriveStructuredBatchCommandNames.
  • __tests__/parity.test.ts — asserts each derived table matches the live hand table. Function-valued traits (daemon closures + capability supports/unsupportedHint) are compared by presence + behavior on a representative request/device sample (mirroring daemon-command-registry.test.ts); every other field is deepEqual'd.

Derivations proven byte-equal

  • Daemon descriptors — order-exact deepEqual against DAEMON_COMMAND_DESCRIPTORS (closures by behavior).
  • Capability matrixdeepEqual against BASE_COMMAND_CAPABILITY_MATRIX (supports/unsupportedHint by behavior across 11 sample devices).
  • Structured-batch names — membership-equal to STRUCTURED_BATCH_COMMAND_NAMES. (Order is not reproduced: the daemon and batch hand tables are independently ordered, so one registry table cannot reproduce both array orders; the consumer dedupes into a Set, so ordering is cosmetic and deferred to a later slice.)

Additive / dormant guarantee

No consumers changed, nothing deleted, no import-graph inversion. The only edits outside src/commands/descriptor/ add an export keyword to three existing tables so the parity tests can read them: DAEMON_COMMAND_DESCRIPTORS + the DaemonCommandDescriptor type (daemon-command-registry.ts) and BASE_COMMAND_CAPABILITY_MATRIX (capabilities.ts). No behavior change.

Verification

  • tsc --noEmit
  • oxfmt --write + oxlint --deny-warnings
  • vitest run commands/descriptor daemon-command-registry capabilities batch → 7 files / 42 tests ✅

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.4 MB 1.4 MB +1.8 kB
JS gzip 445.5 kB 445.7 kB +207 B
npm tarball 584.6 kB 584.6 kB -6 B
npm unpacked 2.0 MB 2.0 MB +1.8 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.4 ms 27.3 ms -1.0 ms
CLI --help 49.1 ms 48.3 ms -0.8 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/9722.js -125 B +65 B
dist/src/session.js +791 B +64 B
dist/src/find.js +363 B +57 B
dist/src/selector-runtime.js +153 B +47 B
dist/src/snapshot.js +142 B +21 B

@thymikee

Copy link
Copy Markdown
Member Author

Finding: the descriptor registry is currently rooted under src/commands/descriptor (registry.ts, types.ts), but the very next slice needs src/daemon/daemon-command-registry.ts to import it. The existing Layering Guard intentionally rejects direct imports from src/daemon or src/platforms into src/commands/*, and #907 already fails on exactly:

  • src/daemon/daemon-command-registry.ts:1: import { deriveDaemonCommandDescriptors } from '../commands/descriptor/derive.ts';
  • src/daemon/daemon-command-registry.ts:2: import { commandDescriptors } from '../commands/descriptor/registry.ts';

That makes this foundation hard to consume as-is. Before merging the foundation, please either move the descriptor root/projections to a neutral shared layer that daemon may import, or update the layering rule with an explicit ADR 0008 exception that still preserves ADR 0003's daemon-owned facet boundary and prevents public projections from reading daemon-only traits. Otherwise #906 lands a dormant registry whose first real consumer cannot pass CI.

I did verify the parity test locally in an isolated worktree: pnpm exec vitest run src/commands/descriptor/__tests__/parity.test.ts passed, 5/5.

@thymikee

Copy link
Copy Markdown
Member Author

Good catch — addressing the layering decision before going further, taking option 1.

I'm relocating the shared descriptor registry + projections out of src/commands/descriptor into a neutral layer the daemon may import: src/core/command-descriptor/. That matches the Layering Guard's stated intent ("keep shared daemon/client contracts outside src/commands") and preserves ADR 0003:

  • The daemon route/policy data is not moved into the public command surfacecore sits below commands/the catalog, so there's no contamination of public identity (the exact thing ADR 0003 rejected).
  • The daemon still owns the predicate interface (isLeaseAdmissionExempt, getDaemonCommandRoute, … stay in daemon-command-registry.ts).
  • The projections stay field-selective, so public projections can't read daemon-only traits.

Relocating across the stack (#906#907#908), re-running the Layering Guard (git grep clean for src/daemon) + tests, and I'll run the pending iOS smoke on the result. Thanks for verifying the parity test locally.

… step 1

Introduce a single dormant CommandDescriptor registry as the strangler-fig
spine for ADR-0008. Per command it carries the daemon route + request-policy
traits (incl. closure traits), an optional capability entry, and
batchable/mcpExposed flags — copied verbatim from today's hand tables.

Pure derive folds reconstruct DAEMON_COMMAND_DESCRIPTORS, the capability
matrix, and the structured-batch names; parity tests prove each derivation
byte-equal to the live hand table. Nothing reads the registry yet.

Purely additive: no consumers changed, nothing deleted, no import inversion.
The only edits outside the new directory expose three existing tables
(DAEMON_COMMAND_DESCRIPTORS, DaemonCommandDescriptor, BASE_COMMAND_CAPABILITY_MATRIX)
so the parity tests can read them.
@thymikee thymikee force-pushed the feat/command-descriptor-registry branch from 97b88ff to 0acefbf Compare June 27, 2026 14:02
thymikee added a commit that referenced this pull request Jun 27, 2026
… — Phase 1 step 3

Build BASE_COMMAND_CAPABILITY_MATRIX from deriveCapabilityMatrix(commandDescriptors)
and delete the hand-authored literal (ADR-0008, Phase 1 step 3). Behaviorless: #906
proved the derived matrix is byte-equal to the literal, including the
supports/unsupportedHint closures across the sample-device matrix.

Delete the now-dead local predicate helpers and capability bundles that only the
literal referenced (isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator,
synthesisGestureUnsupportedHint, ALL_DEVICE_COMMAND_CAPABILITY, APP_* bundles,
LINUX_DEVICE, LINUX_NONE); their copies now live in registry.ts. WEB_* command
sets and addWebCommandCapabilities are kept.

The registry/derive/types modules only type-import CommandCapability from
capabilities.ts, so the new value-level dependency forms no runtime cycle (tsc
clean + runtime import() of capabilities.ts loads with no TDZ/circular-init error).

Rewrite the capability parity tests (now tautologies) into an admission invariant:
every matrix entry is selectable (platform bucket or supports predicate) and the
public-command coverage floor is unchanged. Daemon and batch-name parity tests kept.
@thymikee thymikee merged commit 4648051 into main Jun 27, 2026
20 checks passed
@thymikee thymikee deleted the feat/command-descriptor-registry branch June 27, 2026 16:02
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-27 16:02 UTC

thymikee added a commit that referenced this pull request Jun 27, 2026
… — Phase 1 step 3

Build BASE_COMMAND_CAPABILITY_MATRIX from deriveCapabilityMatrix(commandDescriptors)
and delete the hand-authored literal (ADR-0008, Phase 1 step 3). Behaviorless: #906
proved the derived matrix is byte-equal to the literal, including the
supports/unsupportedHint closures across the sample-device matrix.

Delete the now-dead local predicate helpers and capability bundles that only the
literal referenced (isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator,
synthesisGestureUnsupportedHint, ALL_DEVICE_COMMAND_CAPABILITY, APP_* bundles,
LINUX_DEVICE, LINUX_NONE); their copies now live in registry.ts. WEB_* command
sets and addWebCommandCapabilities are kept.

The registry/derive/types modules only type-import CommandCapability from
capabilities.ts, so the new value-level dependency forms no runtime cycle (tsc
clean + runtime import() of capabilities.ts loads with no TDZ/circular-init error).

Rewrite the capability parity tests (now tautologies) into an admission invariant:
every matrix entry is selectable (platform bucket or supports predicate) and the
public-command coverage floor is unchanged. Daemon and batch-name parity tests kept.
thymikee added a commit that referenced this pull request Jun 27, 2026
… — Phase 1 step 3

Build BASE_COMMAND_CAPABILITY_MATRIX from deriveCapabilityMatrix(commandDescriptors)
and delete the hand-authored literal (ADR-0008, Phase 1 step 3). Behaviorless: #906
proved the derived matrix is byte-equal to the literal, including the
supports/unsupportedHint closures across the sample-device matrix.

Delete the now-dead local predicate helpers and capability bundles that only the
literal referenced (isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator,
synthesisGestureUnsupportedHint, ALL_DEVICE_COMMAND_CAPABILITY, APP_* bundles,
LINUX_DEVICE, LINUX_NONE); their copies now live in registry.ts. WEB_* command
sets and addWebCommandCapabilities are kept.

The registry/derive/types modules only type-import CommandCapability from
capabilities.ts, so the new value-level dependency forms no runtime cycle (tsc
clean + runtime import() of capabilities.ts loads with no TDZ/circular-init error).

Rewrite the capability parity tests (now tautologies) into an admission invariant:
every matrix entry is selectable (platform bucket or supports predicate) and the
public-command coverage floor is unchanged. Daemon and batch-name parity tests kept.
thymikee added a commit that referenced this pull request Jun 27, 2026
… — Phase 1 step 3 (#908)

Build BASE_COMMAND_CAPABILITY_MATRIX from deriveCapabilityMatrix(commandDescriptors)
and delete the hand-authored literal (ADR-0008, Phase 1 step 3). Behaviorless: #906
proved the derived matrix is byte-equal to the literal, including the
supports/unsupportedHint closures across the sample-device matrix.

Delete the now-dead local predicate helpers and capability bundles that only the
literal referenced (isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator,
synthesisGestureUnsupportedHint, ALL_DEVICE_COMMAND_CAPABILITY, APP_* bundles,
LINUX_DEVICE, LINUX_NONE); their copies now live in registry.ts. WEB_* command
sets and addWebCommandCapabilities are kept.

The registry/derive/types modules only type-import CommandCapability from
capabilities.ts, so the new value-level dependency forms no runtime cycle (tsc
clean + runtime import() of capabilities.ts loads with no TDZ/circular-init error).

Rewrite the capability parity tests (now tautologies) into an admission invariant:
every matrix entry is selectable (platform bucket or supports predicate) and the
public-command coverage floor is unchanged. Daemon and batch-name parity tests kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant