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
73 changes: 16 additions & 57 deletions src/batch-policy.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,31 @@
import { PUBLIC_COMMANDS } from './command-catalog.ts';
import { deriveStructuredBatchCommandNames } from './core/command-descriptor/derive.ts';
import { commandDescriptors } from './core/command-descriptor/registry.ts';
import { AppError } from './utils/errors.ts';

/**
* The exact set of command names exposed through `batch`, as a narrow union.
*
* This type is kept HAND-AUTHORED on purpose (ADR-0008, Phase 1 step 4): the
* runtime allowlist below is now derived from the command-descriptor registry,
* but the registry types each `name` as `string`, so deriving the value yields
* `string[]`. Re-deriving the type from that value would WIDEN this union to
* `string` and silently widen its downstream consumers — most notably
* `BatchCommandName` (re-exported from `command-surface.ts`) and the
* `satisfies readonly DaemonCommandName[]` guard in `commands/batch/projection.ts`.
* Keeping the union spelled out preserves those compile-time contracts. The
* derived runtime value is proven to match this union, member-for-member, by
* `core/command-descriptor/__tests__/parity.test.ts`.
* DERIVED from the command-descriptor registry (ADR-0008, Phase 1 step 7): the
* registry is now `as const` (#910), so each entry keeps its literal `name` and
* literal `batchable`. Extracting the entries whose `batchable` is `true` and
* indexing their `name` reconstructs this union from the same single source the
* runtime allowlist below is built from — no hand-maintained list to drift. The
* downstream contracts (`BatchCommandName`, re-exported from `command-surface.ts`,
* and the `satisfies readonly DaemonCommandName[]` guard in
* `commands/batch/projection.ts`) are still enforced by `tsc`.
*/
export type StructuredBatchCommandName =
| (typeof PUBLIC_COMMANDS)['devices']
| (typeof PUBLIC_COMMANDS)['boot']
| (typeof PUBLIC_COMMANDS)['shutdown']
| (typeof PUBLIC_COMMANDS)['apps']
| (typeof PUBLIC_COMMANDS)['open']
| (typeof PUBLIC_COMMANDS)['close']
| (typeof PUBLIC_COMMANDS)['install']
| (typeof PUBLIC_COMMANDS)['reinstall']
| (typeof PUBLIC_COMMANDS)['installFromSource']
| (typeof PUBLIC_COMMANDS)['push']
| (typeof PUBLIC_COMMANDS)['triggerAppEvent']
| (typeof PUBLIC_COMMANDS)['snapshot']
| (typeof PUBLIC_COMMANDS)['screenshot']
| (typeof PUBLIC_COMMANDS)['diff']
| (typeof PUBLIC_COMMANDS)['wait']
| (typeof PUBLIC_COMMANDS)['alert']
| (typeof PUBLIC_COMMANDS)['settings']
| (typeof PUBLIC_COMMANDS)['click']
| (typeof PUBLIC_COMMANDS)['press']
| (typeof PUBLIC_COMMANDS)['longPress']
| (typeof PUBLIC_COMMANDS)['swipe']
| (typeof PUBLIC_COMMANDS)['focus']
| (typeof PUBLIC_COMMANDS)['type']
| (typeof PUBLIC_COMMANDS)['fill']
| (typeof PUBLIC_COMMANDS)['scroll']
| (typeof PUBLIC_COMMANDS)['get']
| (typeof PUBLIC_COMMANDS)['gesture']
| (typeof PUBLIC_COMMANDS)['is']
| (typeof PUBLIC_COMMANDS)['find']
| (typeof PUBLIC_COMMANDS)['perf']
| (typeof PUBLIC_COMMANDS)['logs']
| (typeof PUBLIC_COMMANDS)['network']
| (typeof PUBLIC_COMMANDS)['record']
| (typeof PUBLIC_COMMANDS)['trace']
| (typeof PUBLIC_COMMANDS)['test']
| (typeof PUBLIC_COMMANDS)['appState']
| (typeof PUBLIC_COMMANDS)['back']
| (typeof PUBLIC_COMMANDS)['home']
| (typeof PUBLIC_COMMANDS)['rotate']
| (typeof PUBLIC_COMMANDS)['appSwitcher']
| (typeof PUBLIC_COMMANDS)['keyboard']
| (typeof PUBLIC_COMMANDS)['clipboard']
| (typeof PUBLIC_COMMANDS)['reactNative'];
export type StructuredBatchCommandName = Extract<
(typeof commandDescriptors)[number],
{ batchable: true }
>['name'];

/**
* The structured-batch allowlist, BUILT from the command-descriptor registry
* (the `batchable` flag) rather than hand-maintained. The derived value is a
* (the `batchable` flag) rather than hand-maintained. {@link deriveStructuredBatchCommandNames}
* folds over the registry as `readonly CommandDescriptor[]`, so it returns
* `string[]`; the cast re-applies the narrow {@link StructuredBatchCommandName}
* union, whose membership equality with this value is asserted by the parity test.
* union (which derives from the same `batchable: true` entries). The parity test
* guards the wiring (the exported value equals the derived fold).
*/
export const STRUCTURED_BATCH_COMMAND_NAMES: readonly StructuredBatchCommandName[] =
deriveStructuredBatchCommandNames(commandDescriptors) as readonly StructuredBatchCommandName[];
Expand Down
71 changes: 6 additions & 65 deletions src/core/command-descriptor/__tests__/parity.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
STRUCTURED_BATCH_COMMAND_NAMES,
type StructuredBatchCommandName,
} from '../../../batch-policy.ts';
import { STRUCTURED_BATCH_COMMAND_NAMES } from '../../../batch-policy.ts';
import { PUBLIC_COMMANDS } from '../../../command-catalog.ts';
import { BASE_COMMAND_CAPABILITY_MATRIX } from '../../capabilities.ts';
import {
Expand Down Expand Up @@ -132,56 +129,6 @@ test('capability matrix holds its admission invariants', () => {
}
});

// Exhaustive map over the StructuredBatchCommandName union. The compiler errors
// if a union member is missing a key OR a non-member key is added, so its keys
// ARE the union at type level — giving us a runtime handle on the (otherwise
// hand-authored) union to compare against the derived allowlist.
const STRUCTURED_BATCH_UNION_MEMBERS: Record<StructuredBatchCommandName, true> = {
[PUBLIC_COMMANDS.devices]: true,
[PUBLIC_COMMANDS.boot]: true,
[PUBLIC_COMMANDS.shutdown]: true,
[PUBLIC_COMMANDS.apps]: true,
[PUBLIC_COMMANDS.open]: true,
[PUBLIC_COMMANDS.close]: true,
[PUBLIC_COMMANDS.install]: true,
[PUBLIC_COMMANDS.reinstall]: true,
[PUBLIC_COMMANDS.installFromSource]: true,
[PUBLIC_COMMANDS.push]: true,
[PUBLIC_COMMANDS.triggerAppEvent]: true,
[PUBLIC_COMMANDS.snapshot]: true,
[PUBLIC_COMMANDS.screenshot]: true,
[PUBLIC_COMMANDS.diff]: true,
[PUBLIC_COMMANDS.wait]: true,
[PUBLIC_COMMANDS.alert]: true,
[PUBLIC_COMMANDS.settings]: true,
[PUBLIC_COMMANDS.click]: true,
[PUBLIC_COMMANDS.press]: true,
[PUBLIC_COMMANDS.longPress]: true,
[PUBLIC_COMMANDS.swipe]: true,
[PUBLIC_COMMANDS.focus]: true,
[PUBLIC_COMMANDS.type]: true,
[PUBLIC_COMMANDS.fill]: true,
[PUBLIC_COMMANDS.scroll]: true,
[PUBLIC_COMMANDS.get]: true,
[PUBLIC_COMMANDS.gesture]: true,
[PUBLIC_COMMANDS.is]: true,
[PUBLIC_COMMANDS.find]: true,
[PUBLIC_COMMANDS.perf]: true,
[PUBLIC_COMMANDS.logs]: true,
[PUBLIC_COMMANDS.network]: true,
[PUBLIC_COMMANDS.record]: true,
[PUBLIC_COMMANDS.trace]: true,
[PUBLIC_COMMANDS.test]: true,
[PUBLIC_COMMANDS.appState]: true,
[PUBLIC_COMMANDS.back]: true,
[PUBLIC_COMMANDS.home]: true,
[PUBLIC_COMMANDS.rotate]: true,
[PUBLIC_COMMANDS.appSwitcher]: true,
[PUBLIC_COMMANDS.keyboard]: true,
[PUBLIC_COMMANDS.clipboard]: true,
[PUBLIC_COMMANDS.reactNative]: true,
};

// Control-plane / non-batchable commands that must never enter the allowlist.
const NON_BATCHABLE_COMMANDS = [
PUBLIC_COMMANDS.batch,
Expand All @@ -195,29 +142,23 @@ const NON_BATCHABLE_COMMANDS = [
'transform-gesture',
];

test('structured-batch allowlist is built from descriptors and matches the kept union', () => {
test('structured-batch allowlist is built from descriptors', () => {
const derived = deriveStructuredBatchCommandNames(commandDescriptors);

// No duplicates in the derived allowlist.
assert.equal(new Set(derived).size, derived.length, 'no duplicate batchable names');

// The exported allowlist is now BUILT from these derived descriptors, so it is
// the derived list (order included) — guards the wiring, not a tautology of
// membership.
// the derived list (order included) — guards the wiring. (The narrow
// StructuredBatchCommandName union now DERIVES from the same `batchable: true`
// entries via Extract, so an exhaustive-union membership check would be a
// tautology; `tsc` enforces the type, this guards the value.)
assert.deepEqual(
[...STRUCTURED_BATCH_COMMAND_NAMES],
derived,
'exported allowlist is built from the descriptors',
);

// Membership equals the kept StructuredBatchCommandName union (proves the
// narrow type the consumers rely on did not drift from the runtime value).
assert.deepEqual(
[...new Set(derived)].sort(),
Object.keys(STRUCTURED_BATCH_UNION_MEMBERS).sort(),
'derived membership equals the kept union',
);

// Every batchable command is a real public command (no internal/control name leaks in).
const publicCommands = new Set<string>(Object.values(PUBLIC_COMMANDS));
for (const name of derived) {
Expand Down
Loading