Skip to content

feat: build daemon command registry from descriptors, delete the hand table — Phase 1 step 2 - #907

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

thymikee merged 1 commit into
mainfrom
feat/command-descriptor-daemon-flip

Conversation

@thymikee

Copy link
Copy Markdown
Member

What

Phase 1 step 2 of the command-descriptor migration (ADR-0008), stacked on #906 (feat/command-descriptor-registry).

The daemon command registry now builds from the derived descriptors instead of a hand-authored literal:

  • src/daemon/daemon-command-registry.ts — replaced the DAEMON_COMMAND_DESCRIPTORS array literal (plus its private descriptor/descriptors builders and the isRecordingStartRequest/isShardedTestRequest closures, now living in descriptor/registry.ts) with:
    export const DAEMON_COMMAND_DESCRIPTORS: readonly DaemonCommandDescriptor[] =
      deriveDaemonCommandDescriptors(commandDescriptors);
    The hand literal is deleted. The DaemonCommandDescriptor type, the predicate accessors, and buildDaemonCommandRegistry are unchanged and consume DAEMON_COMMAND_DESCRIPTORS as before.
  • src/commands/descriptor/__tests__/parity.test.ts — the slice-1 DAEMON parity assertion would now be a tautology (derived-vs-derived), so it was replaced with an invariant assertion: no duplicate command names, every descriptor has a route, and the derived command set still covers every public command (minus the two intentionally unrouted app-switcher/install-from-source). The capability-matrix and batch-name parity tests are kept as-is (those hand tables still exist for later slices).

Behaviorless

The daemon registry's routes + request-policy traits are identical — #906's parity test proved deriveDaemonCommandDescriptors(commandDescriptors) is byte-equal to the deleted literal. No predicate accessor or consumer changed.

Cycle check

No runtime import cycle: daemon-command-registry.ts (value) → descriptor/derive.ts + descriptor/registry.ts (values); the back-edges from derive.ts and descriptor/types.ts to this module's DaemonCommandDescriptor are type-only imports, erased at runtime. tsc --noEmit is clean (exit 0) and a runtime import() of the module loads cleanly (57 descriptors).

Verification

  • tsc -p tsconfig.json --noEmit: exit 0
  • oxfmt --write + oxlint --deny-warnings on changed files: exit 0
  • vitest run daemon: 94 files, 902 tests passed
  • vitest run commands/descriptor: 1 file, 5 tests passed

Stacked on #906 — merge that first.

@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 +5.8 kB
JS gzip 445.5 kB 445.9 kB +421 B
npm tarball 584.6 kB 585.1 kB +409 B
npm unpacked 2.0 MB 2.0 MB +5.8 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.2 ms 26.1 ms -1.1 ms
CLI --help 47.1 ms 46.2 ms -0.9 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/9722.js +5.8 kB +421 B

@thymikee

Copy link
Copy Markdown
Member Author

Current blocker: Layering Guard is failing, and the failure is real rather than flaky.

The job reports two forbidden daemon-to-commands imports in src/daemon/daemon-command-registry.ts:

  • import { deriveDaemonCommandDescriptors } from '../commands/descriptor/derive.ts';
  • import { commandDescriptors } from '../commands/descriptor/registry.ts';

This is the same architecture issue from #906: if the descriptor registry is meant to become a root consumed by daemon, it cannot live in the current src/commands/* layer without either moving the shared descriptor/projection modules or intentionally updating the guard for ADR 0008. Please address that layering decision first, then re-run checks; iOS smoke is also still pending.

@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 surface — core 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.

@thymikee
thymikee force-pushed the feat/command-descriptor-registry branch from 97b88ff to 0acefbf Compare June 27, 2026 14:02
@thymikee
thymikee force-pushed the feat/command-descriptor-daemon-flip branch from 95ce046 to 271b173 Compare June 27, 2026 14:05
@thymikee

Copy link
Copy Markdown
Member Author

Both blockers addressed.

Layering fix: relocated the descriptor registry to src/core/command-descriptor/ across the stack (#906→#907→#908). The Layering Guard git grep is now EMPTY on #907 and #908 (git grep -n -E "from '.*commands/" -- src/daemon src/platforms → no matches); old src/commands/descriptor/ removed; bases/stacking intact. tsc + full suites green (907 daemon tests on #907, 1001 on #908). (Rebaser note: a plain rebase resurrects the old dir due to differing patch-ids — used git rebase --onto to replay only the unique top commit.)

iOS smoke (booted iPhone 17 Pro, source build of the #908 tip — the genuinely behavior-relevant checkpoint since the daemon registry + capability matrix now derive at runtime):

  • devices → daemon boots with the derived registries; discovery correct.
  • open com.apple.Preferences → routed through the derived daemon registry + passed the derived capability gate → Opened.
  • snapshot → full path (dispatch + capability + ios-runner build/run) → 109 visible nodes, real AX tree (Apple Account / General / Accessibility / Action Button / …).

No regression — the derived daemon-command-registry + capability-matrix behave identically end-to-end.

Base automatically changed from feat/command-descriptor-registry to main June 27, 2026 16:02
@thymikee
thymikee force-pushed the feat/command-descriptor-daemon-flip branch from 271b173 to b0368aa Compare June 27, 2026 16:22
@thymikee
thymikee merged commit 47abc8c into main Jun 27, 2026
20 checks passed
@thymikee
thymikee deleted the feat/command-descriptor-daemon-flip branch June 27, 2026 17:46
@github-actions

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

thymikee added a commit that referenced this pull request Sep 23, 2026
…ntees (#2779)

`commandDescriptors` claimed to be "proven byte-equal to the live hand tables by
`__tests__/parity.test.ts`". Neither operand survives the ADR 0008 migration. The
test is now `src/__tests__/command-descriptor-parity.test.ts` (moved by 2ec4e91
#2348); the tables it compared were deleted by the PRs that inverted them
(47abc8c #907 daemon routes, 96bc7b1 #908 capability matrix, retired by
ea1d6b8 #2089, 607883d #909 batch allowlist, 8ef4e73 #1137 MCP
exposure); and TIMEOUT_POLICY_BY_COMMAND, DEVICE_CLAIM_POLICY_BY_COMMAND and
COMMAND_DESCRIPTOR_BY_NAME are folds over this array, so byte-equality has no
second operand.

Record the invariant that does hold and name the check that carries each part, so
a future split of this file preserves them. Established by mutation, not by
reading: duplicating a descriptor fails owner-files.test.ts,
command-descriptor-parity.test.ts and device-claim-policy.test.ts, while renaming
one lands on the literal pins (targetIdentityVerification, the timeout and
device-claim deviating sets). No new completeness test was owed here, and the
compile-time totality guard already covers required traits.

Same repair for the `frameworkTier` and `targetIdentityVerification` "the parity
test" pointers, the two `@internal Introspection helper used by parity tests`
docblocks (whose real consumers are command-descriptor-parity.test.ts and
command-surface-metadata.test.ts), the "additive single source" banner that still
listed the retired capability facet, and the CommandDescriptor intro, which
described the deleted tables as "today" and cited `app-switcher` as unrouted
after ADR 0014 gave it a daemon facet.
thymikee added a commit that referenced this pull request Sep 23, 2026
…ecord the descriptor-root block (#2808)

* docs(adr): retire the parity-gate claim, state press-shape refusal, record the descriptor-root block

ADR 0008 still promises a class of check that no longer exists. The parity test its
migration rule required lost its second operand as each hand table was inverted out
(#907, #908, #909, #1084, #1137); the retirement is recorded in Status and the
present-tense claims are corrected, while the migration rule itself is left intact
because it was followed.

ADR 0019 gains the rule that a shape of an operation is not an operation and gets no
fact cell, which is what a per-shape refusal on the owning leaf mechanic rests on.

ADR 0027 records, without deciding, the conflict that makes the descriptor root
unshippably splitable: ADR 0008's synchronous hub and the ADR-0019 eager-closure
module-count budget have no shared approval path. Measured: any decomposition of the
root, even the smallest, grows eleven entry surfaces.

* docs(adr): propose one runtime source for the capability-family cell vocabulary

Adding an operation family edits the cell key twice in one file: once as a property of
UnavailablePlatformRuntimeFacts and again as a value in UNAVAILABLE_CELLS, which cannot
be derived from the type. satisfies makes drift a compile error, so this is a
maintenance tax rather than a soundness hole -- but it is why a file with fan-in 6
churned 9 times in 200 commits, and why one capability still fans out across eight
packages.

Proposes the inversion the repo already proved on the binding axis
(INTERACTOR_OPERATIONS) for the facts axis, and states what it deliberately does not
grant: no default-deny baseline, because ADR 0019 requires exhaustive per-shape facts
and forbids cross-family defaults in a platform package. Gated on a measurement and on
eager-closure neutrality, with withdrawal as an acceptable outcome.

* docs(adr): point ADR 0027 at the pushed, recoverable descriptor split
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