From 396c2607346f42e2af4b59e183ceb6e3413dc8e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 11:45:26 +0000 Subject: [PATCH 1/2] refactor(cli): fold cli-schema into commands/schema, move batch-steps into commands/batch Implements move 1 (+ batch-steps.ts) from the collocation plan in #2677: cli-schema was a rank-0.0003-modularity, 37.5%-legible eight-file zone that existed only to render the command facets commands/ already owns, and batch-steps.ts's only relative import and only importer both already live under commands/. Folding both in removes a family the coupling/legibility measures called out as a clear placement miss, without changing behaviour. - git mv src/cli-schema/* -> src/commands/schema/* - git mv src/cli/batch-steps.ts -> src/commands/batch/batch-steps.ts - update every import site, the literal command-explain.ts owner-files path, the layering model's now-absorbed cli-schema rank entry (folded files resolve to the commands zone by directory), fallow-baselines/ health.json's path-keyed findings, and the AGENTS.md / cli-flags.md pointers Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EmJYA9V4uKcAGBqedwXbwD --- AGENTS.md | 2 +- docs/agents/cli-flags.md | 14 +++++++------- fallow-baselines/health.json | 4 ++-- .../help-conformance-topic-coverage.test.ts | 2 +- scripts/fuzz/targets.ts | 2 +- scripts/fuzz/validation-arbitraries-cli.test.ts | 2 +- scripts/fuzz/validation-arbitraries-cli.ts | 4 ++-- scripts/layering/model.test.ts | 4 ++-- scripts/layering/model.ts | 1 - ...mand-descriptor-post-action-observation.test.ts | 2 +- src/__tests__/command-doc-coverage.test.ts | 4 ++-- src/bin.ts | 2 +- src/cli.ts | 2 +- src/cli/__tests__/command-explain.test.ts | 2 +- src/cli/command-explain.ts | 6 +++--- src/cli/entry.test.ts | 2 +- src/cli/parser/__tests__/args-validation.test.ts | 2 +- .../parser/__tests__/command-suggestions.test.ts | 4 ++-- src/cli/parser/args.ts | 10 +++++----- src/cli/parser/command-suggestions.ts | 4 ++-- src/cli/resolve-cli-options.ts | 4 ++-- .../__tests__/command-surface-metadata.test.ts | 2 +- src/{cli => commands/batch}/batch-steps.ts | 2 +- .../cli-grammar/flag-declaration-admission.test.ts | 4 ++-- src/commands/input-audience.ts | 2 +- src/commands/management/session.test.ts | 2 +- src/{cli-schema => commands/schema}/cli-config.ts | 0 .../schema}/cli-help-alias-fast-path.test.ts | 0 .../schema}/cli-help-command-usage.test.ts | 2 +- .../schema}/cli-help-examples.test.ts | 8 ++++---- .../schema}/cli-help-overview.test.ts | 2 +- .../schema}/cli-help-overview.ts | 0 .../schema}/cli-help-topics.test.ts | 2 +- src/{cli-schema => commands/schema}/cli-help.ts | 0 .../schema}/command-overrides.ts | 4 ++-- .../schema}/command-schema-guards.test.ts | 2 +- .../schema}/command-schema.ts | 2 +- src/{cli-schema => commands/schema}/merge-flags.ts | 0 .../schema}/option-schema.test.ts | 2 +- .../schema}/option-schema.ts | 0 src/{cli-schema => commands/schema}/usage.test.ts | 0 src/{cli-schema => commands/schema}/usage.ts | 0 src/mcp/__tests__/router.test.ts | 2 +- src/mcp/server-guide.ts | 2 +- src/mcp/tool-input-config.ts | 6 +++--- 45 files changed, 62 insertions(+), 63 deletions(-) rename src/{cli => commands/batch}/batch-steps.ts (97%) rename src/{cli-schema => commands/schema}/cli-config.ts (100%) rename src/{cli-schema => commands/schema}/cli-help-alias-fast-path.test.ts (100%) rename src/{cli-schema => commands/schema}/cli-help-command-usage.test.ts (99%) rename src/{cli-schema => commands/schema}/cli-help-examples.test.ts (97%) rename src/{cli-schema => commands/schema}/cli-help-overview.test.ts (97%) rename src/{cli-schema => commands/schema}/cli-help-overview.ts (100%) rename src/{cli-schema => commands/schema}/cli-help-topics.test.ts (99%) rename src/{cli-schema => commands/schema}/cli-help.ts (100%) rename src/{cli-schema => commands/schema}/command-overrides.ts (98%) rename src/{cli-schema => commands/schema}/command-schema-guards.test.ts (98%) rename src/{cli-schema => commands/schema}/command-schema.ts (97%) rename src/{cli-schema => commands/schema}/merge-flags.ts (100%) rename src/{cli-schema => commands/schema}/option-schema.test.ts (99%) rename src/{cli-schema => commands/schema}/option-schema.ts (100%) rename src/{cli-schema => commands/schema}/usage.test.ts (100%) rename src/{cli-schema => commands/schema}/usage.ts (100%) diff --git a/AGENTS.md b/AGENTS.md index 29c7cbc047..213fe6c6ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ Read the declaration rather than maintaining a prose copy: Shared selector parsing and matching belongs in `@agent-device/selectors`; request cancellation and progress in `@agent-device/host-kit/request`; cross-layer contracts in `packages/contracts/src`; -CLI flags in `src/commands/cli-grammar`; cross-surface schema composition in `src/cli-schema`. +CLI flags in `src/commands/cli-grammar`; cross-surface schema composition in `src/commands/schema`. Resolve registry completeness failures at the missing declaration. Diagnose other gate failures at their reported invariant; do not suppress them or add an allowlist to get a pass. Build interaction diff --git a/docs/agents/cli-flags.md b/docs/agents/cli-flags.md index eb1a0c62f6..56b0c3662c 100644 --- a/docs/agents/cli-flags.md +++ b/docs/agents/cli-flags.md @@ -7,9 +7,9 @@ Thread a flag only through the layers that consume it: `packages/command-registry/src/flag-groups.ts` (for example `SNAPSHOT_FLAGS`). Then update the command family metadata/schema that exposes the flag; find the owner with - `rg -n "|supportedFlags|allowedFlags" src/commands src/cli-schema src/cli/parser`. For + `rg -n "|supportedFlags|allowedFlags" src/commands src/cli/parser`. For schema-only CLI commands, the owner is `SCHEMA_ONLY_CLI_COMMAND_SCHEMAS` in - `src/cli-schema/command-overrides.ts`. Every flag declaration states `projectConfig` + `src/commands/schema/command-overrides.ts`. Every flag declaration states `projectConfig` (may be set from a project `agent-device.json`) and `recorded` (the session recorder copies it into `SessionAction.flags`). Both are required, so a new declaration that omits either does not compile — that, not an allowlist, is the completeness gate. Set @@ -43,16 +43,16 @@ steps 1-3, plus step 9. ## Where CLI help and schema live -- Long help prose: `src/cli-schema/cli-help.ts`. Flag definitions: `src/commands/cli-grammar/`. -- Synopsis: `src/cli-schema/usage.ts` generates the `[label]` flag tail from `allowedFlags`, so a +- Long help prose: `src/commands/schema/cli-help.ts`. Flag definitions: `src/commands/cli-grammar/`. +- Synopsis: `src/commands/schema/usage.ts` generates the `[label]` flag tail from `allowedFlags`, so a new option reaches `--help` without any synopsis edit. Declare `usageFlags` on the command only when its synopsis names fewer options: `[]` for a synopsis that is pure grammar (or writes its own mutually-exclusive brackets), otherwise the subset it names. `Command flags:` always lists everything in `allowedFlags`. Keep a cross-cutting opt-in out of every synopsis with - `usageHidden: true` on its flag definition. `src/cli-schema/usage.test.ts` fails a tail that names + `usageHidden: true` on its flag definition. `src/commands/schema/usage.test.ts` fails a tail that names an option the command does not accept, or one the hand-written grammar already wrote. - Command-specific usage/flag metadata lives with the command family metadata that owns the command. - Parser/help *rendering* stays in `src/cli/parser/`; command schema metadata is derived from command metadata, family declarations, and the schema-only merge path in - `src/cli-schema/command-overrides.ts`. Keep the two separate. -- Locating an owner: `rg -n "helpDescription|summary|supportedFlags|allowedFlags" src/commands src/cli/parser src/cli-schema`. + `src/commands/schema/command-overrides.ts`. Keep the two separate. +- Locating an owner: `rg -n "helpDescription|summary|supportedFlags|allowedFlags" src/commands src/cli/parser`. diff --git a/fallow-baselines/health.json b/fallow-baselines/health.json index f6ca2ef984..9076fc5fb6 100644 --- a/fallow-baselines/health.json +++ b/fallow-baselines/health.json @@ -23,12 +23,12 @@ "count": 1 } }, - "src/cli-schema/cli-config.ts": { + "src/commands/schema/cli-config.ts": { "crap_moderate": { "count": 1 } }, - "src/cli-schema/option-schema.ts": { + "src/commands/schema/option-schema.ts": { "complexity_moderate": { "count": 1 }, diff --git a/scripts/__tests__/help-conformance-topic-coverage.test.ts b/scripts/__tests__/help-conformance-topic-coverage.test.ts index 95f66cc90e..80a55664fb 100644 --- a/scripts/__tests__/help-conformance-topic-coverage.test.ts +++ b/scripts/__tests__/help-conformance-topic-coverage.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { CASES } from '../help-conformance-cases.mjs'; -import { helpTopicIds } from '../../src/cli-schema/cli-help.ts'; +import { helpTopicIds } from '../../src/commands/schema/cli-help.ts'; // "What enumerates N": benchmark cases are keyed to help topics, and this gate // keys the case list to the topic registry itself. A new help topic must gain diff --git a/scripts/fuzz/targets.ts b/scripts/fuzz/targets.ts index c344b47eac..e427c64b79 100644 --- a/scripts/fuzz/targets.ts +++ b/scripts/fuzz/targets.ts @@ -10,7 +10,7 @@ import { parseArgs } from '../../src/cli/parser/args.ts'; import { validateSelectorExpression } from '@agent-device/selectors'; import { parseReplayScriptDetailed } from '@agent-device/ad-script'; -import { readCliBatchStepsJson } from '../../src/cli/batch-steps.ts'; +import { readCliBatchStepsJson } from '../../src/commands/batch/batch-steps.ts'; import { inspectMaestroFlow } from '@agent-device/maestro'; import type { FuzzTarget } from './target-types.ts'; import { acceptCase, makeValidationCheck, rejectCase } from './validation-case.ts'; diff --git a/scripts/fuzz/validation-arbitraries-cli.test.ts b/scripts/fuzz/validation-arbitraries-cli.test.ts index b49f807115..88d6406cac 100644 --- a/scripts/fuzz/validation-arbitraries-cli.test.ts +++ b/scripts/fuzz/validation-arbitraries-cli.test.ts @@ -9,7 +9,7 @@ import fc from 'fast-check'; import { describe, expect, it, vi } from 'vitest'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { getFlagDefinitions } from '../../src/cli-schema/command-schema.ts'; +import { getFlagDefinitions } from '../../src/commands/schema/command-schema.ts'; import { getFuzzTarget } from './registry.ts'; import { decodeValidationCase } from './validation-case.ts'; import { describeGeneratorContract } from './validation-generator-contract.ts'; diff --git a/scripts/fuzz/validation-arbitraries-cli.ts b/scripts/fuzz/validation-arbitraries-cli.ts index bb787c7cca..11e323d976 100644 --- a/scripts/fuzz/validation-arbitraries-cli.ts +++ b/scripts/fuzz/validation-arbitraries-cli.ts @@ -13,8 +13,8 @@ import { getCliCommandSchema, getFlagDefinitions, type FlagDefinition, -} from '../../src/cli-schema/command-schema.ts'; -import { isFlagSupportedForCommand } from '../../src/cli-schema/option-schema.ts'; +} from '../../src/commands/schema/command-schema.ts'; +import { isFlagSupportedForCommand } from '../../src/commands/schema/option-schema.ts'; import { encodeValidationCase, type ValidationCase } from './validation-case.ts'; import { ACCEPT, SAFE_VALUES } from './validation-values.ts'; diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 8f5d41df64..0ed0d2c372 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -201,13 +201,13 @@ test('neutral ownership zones reject value imports into higher layers', () => { ['src/commands/cancel.ts', 'export const cancel = true;'], ['packages/selectors/src/internal/parse.ts', "import '../../../../src/client/client.ts';"], ['src/client/client.ts', 'export const client = true;'], - ['src/cli-schema/schema.ts', "import '../cli/parser.ts';"], + ['src/mcp/schema.ts', "import '../cli/parser.ts';"], ['src/cli/parser.ts', 'export const parser = true;'], ]), ); assert.deepEqual(collectBackEdges(edges), { - 'cli-schema -> cli': ['src/cli-schema/schema.ts -> src/cli/parser.ts'], + 'mcp -> cli': ['src/mcp/schema.ts -> src/cli/parser.ts'], 'contracts -> core': ['src/contracts/result.ts -> src/core/result.ts'], 'device-selection -> commands': [ 'packages/device-selection/src/selection.ts -> src/commands/cancel.ts', diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index 9384fd3e62..7818a6bf56 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -55,7 +55,6 @@ const TARGET_DAG_RANK = new Map([ ['selectors', 1], ['session-journal', 1], ['core', 2], - ['cli-schema', 3], ['commands', 3], ['mcp', 3], ['ai-sdk', 4], diff --git a/src/__tests__/command-descriptor-post-action-observation.test.ts b/src/__tests__/command-descriptor-post-action-observation.test.ts index d12ebaeeaa..fde338f035 100644 --- a/src/__tests__/command-descriptor-post-action-observation.test.ts +++ b/src/__tests__/command-descriptor-post-action-observation.test.ts @@ -11,7 +11,7 @@ import { listCommandFamilyDefinitions, } from '../commands/family/registry.ts'; import type { SettleCapableClientOptionCommands } from '../commands/post-action-observation-client-options.ts'; -import { getCliCommandSchema } from '../cli-schema/command-schema.ts'; +import { getCliCommandSchema } from '../commands/schema/command-schema.ts'; import { buildActionDetails } from '@agent-device/session-journal/session-event-action'; import { COMMAND_OUTPUT_SCHEMAS } from '../mcp/command-output-schemas.ts'; import { diff --git a/src/__tests__/command-doc-coverage.test.ts b/src/__tests__/command-doc-coverage.test.ts index 6ad7824c54..6e6f30156d 100644 --- a/src/__tests__/command-doc-coverage.test.ts +++ b/src/__tests__/command-doc-coverage.test.ts @@ -3,8 +3,8 @@ import assert from 'node:assert/strict'; import { describe, test } from 'vitest'; import { PUBLIC_COMMANDS, isKnownCliCommandName } from '@agent-device/command-registry/catalog'; import { cliCommandAlias } from '@agent-device/command-registry/cli-command-aliases'; -import { getCliCommandSchema } from '../cli-schema/command-schema.ts'; -import { buildCommandUsage } from '../cli-schema/usage.ts'; +import { getCliCommandSchema } from '../commands/schema/command-schema.ts'; +import { buildCommandUsage } from '../commands/schema/usage.ts'; // Enumerates the public command surface (`PUBLIC_COMMANDS`, derived from the // command descriptor registry) against the human-facing command reference at diff --git a/src/bin.ts b/src/bin.ts index 34d30c9f19..1763cfff55 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -5,7 +5,7 @@ declare const __AGENT_DEVICE_VERSION__: string; runEntry( process.argv.slice(2), { - help: () => import('./cli-schema/cli-help.ts'), + help: () => import('./commands/schema/cli-help.ts'), cli: () => import('./cli/process-entry.ts'), mcp: () => import('./mcp/server.ts'), version: () => import('@agent-device/host-kit/version'), diff --git a/src/cli.ts b/src/cli.ts index 0ed00b43c4..67246cd596 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -29,7 +29,7 @@ import { materializeRemoteConnectionForCommand } from './cli/commands/connection import { tryRunClientBackedCommand } from './cli/commands/router.ts'; import { runAgentCdpCommand } from './cli/commands/agent-cdp.ts'; import { runReactDevtoolsCommand } from './cli/commands/react-devtools.ts'; -import { readCliBatchStepsJson } from './cli/batch-steps.ts'; +import { readCliBatchStepsJson } from './commands/batch/batch-steps.ts'; import { createRequestId, emitDiagnostic, diff --git a/src/cli/__tests__/command-explain.test.ts b/src/cli/__tests__/command-explain.test.ts index 56445b0571..408c910412 100644 --- a/src/cli/__tests__/command-explain.test.ts +++ b/src/cli/__tests__/command-explain.test.ts @@ -93,7 +93,7 @@ describe('explainCommand', () => { cli: { usage: 'web setup | web doctor' }, files: expect.arrayContaining([ 'packages/command-registry/src/registry.ts', - 'src/cli-schema/command-overrides.ts', + 'src/commands/schema/command-overrides.ts', 'src/cli/commands/web.ts', ]), }, diff --git a/src/cli/command-explain.ts b/src/cli/command-explain.ts index cf966e69e5..a22cb9338b 100644 --- a/src/cli/command-explain.ts +++ b/src/cli/command-explain.ts @@ -3,7 +3,7 @@ import { cliAliasesForCommand, normalizeCliCommandAlias, } from '@agent-device/command-registry/cli-command-aliases'; -import { buildCommandUsage } from '../cli-schema/usage.ts'; +import { buildCommandUsage } from '../commands/schema/usage.ts'; import type { DaemonCommandRoute } from '../daemon/daemon-command-registry.ts'; import { commandDescriptors, type Command } from '@agent-device/command-registry/registry'; import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; @@ -15,7 +15,7 @@ import { type CommandSchema, type FlagDefinition, type FlagKey, -} from '../cli-schema/command-schema.ts'; +} from '../commands/schema/command-schema.ts'; import { commandFamilies, type CommandFamilyMetadata } from '../commands/family/registry.ts'; export type CommandFlagExplanation = { @@ -317,7 +317,7 @@ function commandFiles( `src/commands/${family}/index.test.ts`, ); } else if (cliCommandNames.has(command)) { - derived.push('src/cli-schema/command-overrides.ts'); + derived.push('src/commands/schema/command-overrides.ts'); } if (daemonRoute) derived.push(daemonRouteOwnerFiles[daemonRoute]); const present = fileExists ? opportunistic.filter(fileExists) : opportunistic; diff --git a/src/cli/entry.test.ts b/src/cli/entry.test.ts index 13a3836919..f607dfe020 100644 --- a/src/cli/entry.test.ts +++ b/src/cli/entry.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import * as cliHelp from '../cli-schema/cli-help.ts'; +import * as cliHelp from '../commands/schema/cli-help.ts'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; import { cliAliasesForCommand } from '@agent-device/command-registry/cli-command-aliases'; import { runEntry, type EntryModules } from './entry.ts'; diff --git a/src/cli/parser/__tests__/args-validation.test.ts b/src/cli/parser/__tests__/args-validation.test.ts index 22ecfeb669..2f729d841d 100644 --- a/src/cli/parser/__tests__/args-validation.test.ts +++ b/src/cli/parser/__tests__/args-validation.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { parseArgs } from '../args.ts'; import { AppError } from '@agent-device/kernel/errors'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { getCliCommandSchema } from '../../../cli-schema/command-schema.ts'; +import { getCliCommandSchema } from '../../../commands/schema/command-schema.ts'; test('parseArgs rejects test retries above the supported ceiling', () => { assert.throws( diff --git a/src/cli/parser/__tests__/command-suggestions.test.ts b/src/cli/parser/__tests__/command-suggestions.test.ts index 9c0191b914..dc9e039e86 100644 --- a/src/cli/parser/__tests__/command-suggestions.test.ts +++ b/src/cli/parser/__tests__/command-suggestions.test.ts @@ -5,8 +5,8 @@ import { keyboardCliReader } from '../../../commands/system/index.ts'; import { AppError } from '@agent-device/kernel/errors'; import { parseArgs } from '../args.ts'; import type { CliFlags } from '@agent-device/contracts/command'; -import { getFlagDefinitions } from '../../../cli-schema/command-schema.ts'; -import { isFlagSupportedForCommand } from '../../../cli-schema/option-schema.ts'; +import { getFlagDefinitions } from '../../../commands/schema/command-schema.ts'; +import { isFlagSupportedForCommand } from '../../../commands/schema/option-schema.ts'; import { listCommandAliasSuggestionEntries, listFlagNameSuggestionEntries, diff --git a/src/cli/parser/args.ts b/src/cli/parser/args.ts index 93fc29755a..fae48dad9a 100644 --- a/src/cli/parser/args.ts +++ b/src/cli/parser/args.ts @@ -1,6 +1,6 @@ import type { CliFlags } from '@agent-device/contracts/command'; import { AppError } from '@agent-device/kernel/errors'; -import { mergeDefinedFlags } from '../../cli-schema/merge-flags.ts'; +import { mergeDefinedFlags } from '../../commands/schema/merge-flags.ts'; import { assertCommandPositionalArity, getCommandSchema, @@ -8,8 +8,8 @@ import { getFlagDefinitions, type FlagDefinition, type FlagKey, -} from '../../cli-schema/command-schema.ts'; -import { isFlagSupportedForCommand } from '../../cli-schema/option-schema.ts'; +} from '../../commands/schema/command-schema.ts'; +import { isFlagSupportedForCommand } from '../../commands/schema/option-schema.ts'; import { applyCommandDefaults } from '@agent-device/command-registry/registry'; import { isKnownCliCommandName } from '@agent-device/command-registry/catalog'; import { @@ -387,12 +387,12 @@ function formatUnsupportedFlagMessage(command: string | null, unsupported: strin // Usage text lives in cli-help.ts, which pulls the full command schema surface. // Callers load it lazily so plain command invocations never parse the help text. export async function usage(): Promise { - const { buildUsageText } = await import('../../cli-schema/cli-help.ts'); + const { buildUsageText } = await import('../../commands/schema/cli-help.ts'); return buildUsageText(); } export async function usageForCommand(command: string): Promise { - const { buildCommandUsageText } = await import('../../cli-schema/cli-help.ts'); + const { buildCommandUsageText } = await import('../../commands/schema/cli-help.ts'); return buildCommandUsageText(normalizeCommandAlias(command)); } diff --git a/src/cli/parser/command-suggestions.ts b/src/cli/parser/command-suggestions.ts index 5acb916df1..fbb651bacb 100644 --- a/src/cli/parser/command-suggestions.ts +++ b/src/cli/parser/command-suggestions.ts @@ -1,7 +1,7 @@ import { RETIRED_SCREENSHOT_MAX_SIZE } from '@agent-device/contracts/capture'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { getFlagDefinitions } from '../../cli-schema/command-schema.ts'; -import { isFlagSupportedForCommand } from '../../cli-schema/option-schema.ts'; +import { getFlagDefinitions } from '../../commands/schema/command-schema.ts'; +import { isFlagSupportedForCommand } from '../../commands/schema/option-schema.ts'; /** * Curated guess -> canonical command mapping for unknown CLI command names. diff --git a/src/cli/resolve-cli-options.ts b/src/cli/resolve-cli-options.ts index 8bbfcc3242..96ff93bb51 100644 --- a/src/cli/resolve-cli-options.ts +++ b/src/cli/resolve-cli-options.ts @@ -1,7 +1,7 @@ import type { CliFlags } from '@agent-device/contracts/command'; -import { mergeDefinedFlags } from '../cli-schema/merge-flags.ts'; +import { mergeDefinedFlags } from '../commands/schema/merge-flags.ts'; import { finalizeParsedArgs, parseRawArgs } from './parser/args.ts'; -import { resolveConfigBackedFlagDefaults } from '../cli-schema/cli-config.ts'; +import { resolveConfigBackedFlagDefaults } from '../commands/schema/cli-config.ts'; import { resolveRemoteConfigDefaults } from './remote-config-flags.ts'; import { type EnvMap } from '@agent-device/kernel/source-value'; diff --git a/src/commands/__tests__/command-surface-metadata.test.ts b/src/commands/__tests__/command-surface-metadata.test.ts index 1fb4ca173d..3f250e558b 100644 --- a/src/commands/__tests__/command-surface-metadata.test.ts +++ b/src/commands/__tests__/command-surface-metadata.test.ts @@ -5,7 +5,7 @@ import { listCommandResponseDataTransforms, listMcpExposedCommandNames, } from '@agent-device/command-registry/registry'; -import { getSchemaOnlyCliCommandSchema } from '../../cli-schema/command-overrides.ts'; +import { getSchemaOnlyCliCommandSchema } from '../schema/command-overrides.ts'; import { listCommandMetadata, listCommandMetadataNames, diff --git a/src/cli/batch-steps.ts b/src/commands/batch/batch-steps.ts similarity index 97% rename from src/cli/batch-steps.ts rename to src/commands/batch/batch-steps.ts index 5202c4744d..dd10ac5b74 100644 --- a/src/cli/batch-steps.ts +++ b/src/commands/batch/batch-steps.ts @@ -10,7 +10,7 @@ import { BATCH_AVAILABLE_COMMANDS_HINT, readStructuredBatchCommandName, } from '@agent-device/command-registry/batch-policy'; -import { assertAllowedKeys } from '../commands/input-readers.ts'; +import { assertAllowedKeys } from '../input-readers.ts'; /** * The terminal half of the step-shape refusal. `@agent-device/contracts` states the shape for diff --git a/src/commands/cli-grammar/flag-declaration-admission.test.ts b/src/commands/cli-grammar/flag-declaration-admission.test.ts index 5891055a51..46df79b5c5 100644 --- a/src/commands/cli-grammar/flag-declaration-admission.test.ts +++ b/src/commands/cli-grammar/flag-declaration-admission.test.ts @@ -5,7 +5,7 @@ import { test, vi } from 'vitest'; import type { CliFlags } from '@agent-device/contracts/command'; import { getFlagDefinitionsForKey } from '@agent-device/command-registry/flag-registry'; import type { FlagDefinition } from '@agent-device/command-registry/flag-types'; -import { resolveConfigBackedFlagDefaults } from '../../cli-schema/cli-config.ts'; +import { resolveConfigBackedFlagDefaults } from '../schema/cli-config.ts'; import { recordActionEntry } from '../../daemon/session-action-recorder.ts'; import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts'; import { makeTempWorkspace } from '../../__tests__/cli-config-fixtures.ts'; @@ -63,7 +63,7 @@ test('project-config admission follows the declaration: an undeclared key is ref const planted = getInRegistry(registry, 'daemonBaseUrl'); Object.assign(planted, { projectConfig: true }); const { resolveConfigBackedFlagDefaults: derivePlanted } = - await import('../../cli-schema/cli-config.ts'); + await import('../schema/cli-config.ts'); const workspace = makeTempWorkspace(); try { writeProjectConfig(workspace.project, { daemonBaseUrl: 'https://daemon.example.test' }); diff --git a/src/commands/input-audience.ts b/src/commands/input-audience.ts index 17edee193f..3d7a73e61d 100644 --- a/src/commands/input-audience.ts +++ b/src/commands/input-audience.ts @@ -15,7 +15,7 @@ export type OperatorInputSource = | { envFlagKeys?: readonly [string, ...string[]]; operatorConfig?: boolean } /** * No environment variable (the `ENV_EXCLUDED_FLAG_KEYS` set in - * `cli-schema/option-schema.ts`), so the config file is the only path. + * `schema/option-schema.ts`), so the config file is the only path. */ | { envFlagKeys: readonly []; operatorConfig: true } /** Neither env nor config resolves it: the declaration states its own sentence. */ diff --git a/src/commands/management/session.test.ts b/src/commands/management/session.test.ts index fc30658ed3..fa7829c0c4 100644 --- a/src/commands/management/session.test.ts +++ b/src/commands/management/session.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { createAgentDeviceClient } from '../../agent-device-client.ts'; import { parseArgs } from '../../cli/parser/args.ts'; -import { buildCommandUsageText } from '../../cli-schema/cli-help.ts'; +import { buildCommandUsageText } from '../schema/cli-help.ts'; import type { DaemonRequest, DaemonResponse } from '@agent-device/kernel/contracts'; import type { CliFlags } from '@agent-device/contracts/command'; import { sessionCommandFacet } from './session.ts'; diff --git a/src/cli-schema/cli-config.ts b/src/commands/schema/cli-config.ts similarity index 100% rename from src/cli-schema/cli-config.ts rename to src/commands/schema/cli-config.ts diff --git a/src/cli-schema/cli-help-alias-fast-path.test.ts b/src/commands/schema/cli-help-alias-fast-path.test.ts similarity index 100% rename from src/cli-schema/cli-help-alias-fast-path.test.ts rename to src/commands/schema/cli-help-alias-fast-path.test.ts diff --git a/src/cli-schema/cli-help-command-usage.test.ts b/src/commands/schema/cli-help-command-usage.test.ts similarity index 99% rename from src/cli-schema/cli-help-command-usage.test.ts rename to src/commands/schema/cli-help-command-usage.test.ts index afc4790be3..ae965b9697 100644 --- a/src/cli-schema/cli-help-command-usage.test.ts +++ b/src/commands/schema/cli-help-command-usage.test.ts @@ -1,7 +1,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; -import { usageForCommand } from '../cli/parser/args.ts'; +import { usageForCommand } from '../../cli/parser/args.ts'; test('usageForCommand documents open --launch-args', async () => { const help = await usageForCommand('open'); diff --git a/src/cli-schema/cli-help-examples.test.ts b/src/commands/schema/cli-help-examples.test.ts similarity index 97% rename from src/cli-schema/cli-help-examples.test.ts rename to src/commands/schema/cli-help-examples.test.ts index 24c0c399cc..05849bf6f1 100644 --- a/src/cli-schema/cli-help-examples.test.ts +++ b/src/commands/schema/cli-help-examples.test.ts @@ -1,12 +1,12 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import type { BatchStep } from '@agent-device/contracts/client'; -import { parseArgs } from '../cli/parser/args.ts'; -import { readCliBatchStepsJson } from '../cli/batch-steps.ts'; +import { parseArgs } from '../../cli/parser/args.ts'; +import { readCliBatchStepsJson } from '../batch/batch-steps.ts'; import { buildCommandUsageText, buildUsageText, helpTopicIds } from './cli-help.ts'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { readInputFromCli } from '../commands/cli-grammar.ts'; -import { findCommandMetadata, isCommandName } from '../commands/command-metadata.ts'; +import { readInputFromCli } from '../cli-grammar.ts'; +import { findCommandMetadata, isCommandName } from '../command-metadata.ts'; import { readVersion } from '@agent-device/host-kit/version'; // Help is the agent-facing contract, and agents copy its example lines verbatim, so an example the diff --git a/src/cli-schema/cli-help-overview.test.ts b/src/commands/schema/cli-help-overview.test.ts similarity index 97% rename from src/cli-schema/cli-help-overview.test.ts rename to src/commands/schema/cli-help-overview.test.ts index a468a987e4..00c75a0310 100644 --- a/src/cli-schema/cli-help-overview.test.ts +++ b/src/commands/schema/cli-help-overview.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { usage, usageForCommand } from '../cli/parser/args.ts'; +import { usage, usageForCommand } from '../../cli/parser/args.ts'; test('root help is a bounded first-success decision card', async () => { const help = await usage(); diff --git a/src/cli-schema/cli-help-overview.ts b/src/commands/schema/cli-help-overview.ts similarity index 100% rename from src/cli-schema/cli-help-overview.ts rename to src/commands/schema/cli-help-overview.ts diff --git a/src/cli-schema/cli-help-topics.test.ts b/src/commands/schema/cli-help-topics.test.ts similarity index 99% rename from src/cli-schema/cli-help-topics.test.ts rename to src/commands/schema/cli-help-topics.test.ts index 157a6b0ba0..212235fd3e 100644 --- a/src/cli-schema/cli-help-topics.test.ts +++ b/src/commands/schema/cli-help-topics.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { usage, usageForCommand } from '../cli/parser/args.ts'; +import { usage, usageForCommand } from '../../cli/parser/args.ts'; test('commands topic includes concise command catalog entries', async () => { const usageText = await usageForCommand('commands'); diff --git a/src/cli-schema/cli-help.ts b/src/commands/schema/cli-help.ts similarity index 100% rename from src/cli-schema/cli-help.ts rename to src/commands/schema/cli-help.ts diff --git a/src/cli-schema/command-overrides.ts b/src/commands/schema/command-overrides.ts similarity index 98% rename from src/cli-schema/command-overrides.ts rename to src/commands/schema/command-overrides.ts index dce147c149..728c32ffd1 100644 --- a/src/cli-schema/command-overrides.ts +++ b/src/commands/schema/command-overrides.ts @@ -1,5 +1,5 @@ -import type { CommandName } from '../commands/command-metadata.ts'; -import { listCommandFamilyCliSchemas } from '../commands/family/registry.ts'; +import type { CommandName } from '../command-metadata.ts'; +import { listCommandFamilyCliSchemas } from '../family/registry.ts'; import type { LocalCliCommandName } from '@agent-device/command-registry/catalog'; import type { CommandSchema } from '@agent-device/command-registry/command-schema'; import { diff --git a/src/cli-schema/command-schema-guards.test.ts b/src/commands/schema/command-schema-guards.test.ts similarity index 98% rename from src/cli-schema/command-schema-guards.test.ts rename to src/commands/schema/command-schema-guards.test.ts index bf94ad4e8a..beaa1d70ee 100644 --- a/src/cli-schema/command-schema-guards.test.ts +++ b/src/commands/schema/command-schema-guards.test.ts @@ -73,7 +73,7 @@ test('schema runtime-fact mappings match descriptor source-of-truth', () => { }); function collectCliDispatchCommandLiterals(): Set { - const cliPath = fileURLToPath(new URL('../cli.ts', import.meta.url)); + const cliPath = fileURLToPath(new URL('../../cli.ts', import.meta.url)); const sourceText = fs.readFileSync(cliPath, 'utf8'); const parsed = parseSync(cliPath, sourceText); const commands = new Set(); diff --git a/src/cli-schema/command-schema.ts b/src/commands/schema/command-schema.ts similarity index 97% rename from src/cli-schema/command-schema.ts rename to src/commands/schema/command-schema.ts index ba02aa4593..06c609e8ca 100644 --- a/src/cli-schema/command-schema.ts +++ b/src/commands/schema/command-schema.ts @@ -1,5 +1,5 @@ import type { CliCommandName } from '@agent-device/command-registry/catalog'; -import { listCommandMetadata } from '../commands/command-metadata.ts'; +import { listCommandMetadata } from '../command-metadata.ts'; import type { CommandSchema } from '@agent-device/command-registry/command-schema'; import { getCliCommandOverride, getSchemaOnlyCliCommandSchema } from './command-overrides.ts'; import { diff --git a/src/cli-schema/merge-flags.ts b/src/commands/schema/merge-flags.ts similarity index 100% rename from src/cli-schema/merge-flags.ts rename to src/commands/schema/merge-flags.ts diff --git a/src/cli-schema/option-schema.test.ts b/src/commands/schema/option-schema.test.ts similarity index 99% rename from src/cli-schema/option-schema.test.ts rename to src/commands/schema/option-schema.test.ts index f8963c81eb..e893ee7ca1 100644 --- a/src/cli-schema/option-schema.test.ts +++ b/src/commands/schema/option-schema.test.ts @@ -11,7 +11,7 @@ import { AppError } from '@agent-device/kernel/errors'; import { REMOTE_CONFIG_FIELD_SPECS, getRemoteConfigEnvNames, -} from '../remote/remote-config-schema.ts'; +} from '../../remote/remote-config-schema.ts'; test('option schema exposes config/env metadata for global options', () => { const spec = getOptionSpec('platform'); diff --git a/src/cli-schema/option-schema.ts b/src/commands/schema/option-schema.ts similarity index 100% rename from src/cli-schema/option-schema.ts rename to src/commands/schema/option-schema.ts diff --git a/src/cli-schema/usage.test.ts b/src/commands/schema/usage.test.ts similarity index 100% rename from src/cli-schema/usage.test.ts rename to src/commands/schema/usage.test.ts diff --git a/src/cli-schema/usage.ts b/src/commands/schema/usage.ts similarity index 100% rename from src/cli-schema/usage.ts rename to src/commands/schema/usage.ts diff --git a/src/mcp/__tests__/router.test.ts b/src/mcp/__tests__/router.test.ts index 0a1f406971..92cb24aa10 100644 --- a/src/mcp/__tests__/router.test.ts +++ b/src/mcp/__tests__/router.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { setImmediate } from 'node:timers/promises'; import { test } from 'vitest'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; -import { helpTopicIds } from '../../cli-schema/cli-help.ts'; +import { helpTopicIds } from '../../commands/schema/cli-help.ts'; import { listMcpExposedCommandNames } from '@agent-device/command-registry/registry'; import { handleMcpMessage } from '../router.ts'; import { diff --git a/src/mcp/server-guide.ts b/src/mcp/server-guide.ts index e94dbd6050..b7dba19c8c 100644 --- a/src/mcp/server-guide.ts +++ b/src/mcp/server-guide.ts @@ -3,7 +3,7 @@ import { buildUsageText, helpTopicIds, resolveHelpTargetUsageText, -} from '../cli-schema/cli-help.ts'; +} from '../commands/schema/cli-help.ts'; import { listCliCommandNames } from '@agent-device/command-registry/catalog'; import { listMcpExposedCommandNames } from '@agent-device/command-registry/registry'; import type { ToolResult } from './command-tools.ts'; diff --git a/src/mcp/tool-input-config.ts b/src/mcp/tool-input-config.ts index c26f027e2a..9c907eb2f5 100644 --- a/src/mcp/tool-input-config.ts +++ b/src/mcp/tool-input-config.ts @@ -1,10 +1,10 @@ -import { isFlagSupportedForCommand } from '../cli-schema/option-schema.ts'; +import { isFlagSupportedForCommand } from '../commands/schema/option-schema.ts'; import type { FlagKey } from '@agent-device/command-registry/flag-types'; import type { CliFlags } from '@agent-device/contracts/command'; import type { JsonSchema } from '../commands/command-contract.ts'; import type { CommandName } from '../commands/command-metadata.ts'; -import { resolveConfigBackedFlagDefaults } from '../cli-schema/cli-config.ts'; -import { mergeDefinedFlags } from '../cli-schema/merge-flags.ts'; +import { resolveConfigBackedFlagDefaults } from '../commands/schema/cli-config.ts'; +import { mergeDefinedFlags } from '../commands/schema/merge-flags.ts'; export function resolveMcpConfigDefaults( name: CommandName, From b03e8cf438c43577455e0be723ae3340cb0e87b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 12:24:36 +0000 Subject: [PATCH 2/2] fix(layering): re-enforce commands-schema-floor after cli-schema folded into it Review on #2678 found a real enforcement gap: folding cli-schema into commands/schema/ put both sides of the #2543 "commands never imports cli-schema back" direction into the same commands zone, which checkLayeringRules's ZONE_POLICIES table cannot see (it skips every same-zone edge before the table ever runs). The R2 zone-policy row that used to declare the direction became silently unenforceable, not just harmlessly dead like the daemon-side check. - add scripts/layering/commands-schema-boundary.ts: a folder-scoped check (commands outside schema/ must not import commands/schema/) wired into check.ts's rule registry, replacing the lost R2 row - drop the now-unenforceable R2 row from zone-policy.ts's table and its test, and the header comments that described it - delete the actually-dead checkDaemonCliSchemaBoundary from daemon-modularity.ts (the daemon side stays covered generically by R2's daemon/core -> commands row) and its tests Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EmJYA9V4uKcAGBqedwXbwD --- scripts/layering/check.ts | 12 ++-- .../layering/commands-schema-boundary.test.ts | 64 +++++++++++++++++++ scripts/layering/commands-schema-boundary.ts | 43 +++++++++++++ scripts/layering/daemon-modularity.test.ts | 32 ---------- scripts/layering/daemon-modularity.ts | 25 -------- scripts/layering/zone-policy.test.ts | 16 ----- scripts/layering/zone-policy.ts | 20 ++---- 7 files changed, 122 insertions(+), 90 deletions(-) create mode 100644 scripts/layering/commands-schema-boundary.test.ts create mode 100644 scripts/layering/commands-schema-boundary.ts diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index f9122f8a85..2d135cf4fb 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -3,13 +3,14 @@ // // Ranked target spine, as rank groups lowest to highest. `A ◄ B` means B may not // be outranked by A (the back-edge order the gate rejects), NOT that every displayed import exists: -// { contracts, request, selectors } ◄ core ◄ { commands, cli-schema } +// { contracts, request, selectors } ◄ core ◄ commands // ◄ { client, daemon-server } ◄ daemon-client ◄ cli // (authoritative ranks: `TARGET_DAG_RANK` in model.ts. The former rank-0 kernel // zone lives in packages/kernel since #1490 W0; R11 owns its boundary.) -// `commands` and `cli-schema` share a rank, so the spine cannot order them; R2 declares the -// direction instead — cli-schema renders the command facets and reads them, commands never -// imports cli-schema (#2543). +// `commands/schema/` renders the rest of commands/'s facets and reads them; commands never +// imports it back (#2543). It shares the commands zone (#2679 folded the standalone +// cli-schema zone into it), so the ranked spine and the R2 zone-policy table cannot see the +// edge — `commands-schema-boundary.ts` enforces the direction as a folder-scoped rule instead. // // This gate enforces five things, across four scopes: // - GLOBALLY, across every production source file: the remaining R2 move rule and @@ -103,6 +104,7 @@ import { readTrackedPlatformPackageDeclarations, } from './platform-package-repository.ts'; import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts'; +import { checkCommandsSchemaBoundary } from './commands-schema-boundary.ts'; import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts'; import { substrateDomainShapeViolations } from './substrate-domain-shape.ts'; import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts'; @@ -436,6 +438,7 @@ export type LayeringRule = (context: LayeringContext) => LayeringViolation[]; */ export const LAYERING_RULE_IDS = [ 'zone-policies', + 'commands-schema-boundary', 'value-import-cycles', 'runtime-execution-integrity', 'source-execution-compatibility', @@ -468,6 +471,7 @@ export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number]; export const LAYERING_RULES: Readonly> = { 'zone-policies': (context) => checkLayeringRules(context.edges), + 'commands-schema-boundary': (context) => checkCommandsSchemaBoundary(context.edges), 'value-import-cycles': (context) => checkCycles(context.edges), 'runtime-execution-integrity': (context) => runtimeExecutionIntegrityViolations(context.sources), 'source-execution-compatibility': (context) => diff --git a/scripts/layering/commands-schema-boundary.test.ts b/scripts/layering/commands-schema-boundary.test.ts new file mode 100644 index 0000000000..b003827659 --- /dev/null +++ b/scripts/layering/commands-schema-boundary.test.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { checkCommandsSchemaBoundary } from './commands-schema-boundary.ts'; +import { targetDagZone, type ResolvedImportEdge } from './model.ts'; + +function importEdge( + file: string, + target: string, + overrides: Partial = {}, +): ResolvedImportEdge { + return { + file, + target, + spec: target, + line: 1, + dynamic: false, + typeOnly: false, + symbols: [], + bindingResidue: false, + fromZone: targetDagZone(file), + toZone: targetDagZone(target), + ...overrides, + }; +} + +test('commands outside schema/ must not import commands/schema/', () => { + const edge = importEdge('src/commands/capture/wait.ts', 'src/commands/schema/command-schema.ts'); + const violations = checkCommandsSchemaBoundary([edge]); + assert.equal(violations.length, 1); + assert.equal(violations[0]!.rule, 'R2 commands-floor'); + assert.match(violations[0]!.message, /must not import src\/commands\/schema\/command-schema\.ts/); +}); + +test('holds for value, type-only, and dynamic imports alike', () => { + for (const overrides of [{}, { typeOnly: true }, { dynamic: true }]) { + const edge = importEdge( + 'src/commands/capture/wait.ts', + 'src/commands/schema/command-schema.ts', + overrides, + ); + assert.equal(checkCommandsSchemaBoundary([edge]).length, 1); + } +}); + +test('the declared direction — schema reading the rest of commands — stays silent', () => { + const edge = importEdge( + 'src/commands/schema/command-schema.ts', + 'src/commands/command-metadata.ts', + ); + assert.deepEqual(checkCommandsSchemaBoundary([edge]), []); +}); + +test('schema importing its own sibling files stays silent', () => { + const edge = importEdge( + 'src/commands/schema/cli-help.ts', + 'src/commands/schema/command-schema.ts', + ); + assert.deepEqual(checkCommandsSchemaBoundary([edge]), []); +}); + +test('a non-commands zone importing commands/schema/ is not this rule’s concern', () => { + const edge = importEdge('src/mcp/server-guide.ts', 'src/commands/schema/cli-help.ts'); + assert.deepEqual(checkCommandsSchemaBoundary([edge]), []); +}); diff --git a/scripts/layering/commands-schema-boundary.ts b/scripts/layering/commands-schema-boundary.ts new file mode 100644 index 0000000000..f693ba70ab --- /dev/null +++ b/scripts/layering/commands-schema-boundary.ts @@ -0,0 +1,43 @@ +// Catches: a file under src/commands/ (outside commands/schema/) importing back into +// commands/schema/ — the render-the-facets direction #2543 declared (cli-schema reads +// commands, commands never reads cli-schema back). #2679 folded the standalone cli-schema +// zone into commands/schema/, so the two now share one zone and the ranked-spine / R2 +// zone-policy table can no longer see the edge at all: checkLayeringRules in check.ts skips +// every same-zone edge before ZONE_POLICIES ever runs. This is the folder-scoped +// replacement for that lost R2 row. +// Evidence: #2543 declared the direction as the second R2 commands-floor row in +// zone-policy.ts; #2679 moved src/cli-schema/* into src/commands/schema/* and found the +// direction unenforced on the merged tree. +// Cost: ~30 LOC (rule + test). +// Kill criterion: none enforced today; retire only by maintainer decision that commands/ +// reading its own schema layer back is fine, or that commands/schema/ should move out of +// the commands zone entirely (at which point the ranked-spine R2 table can express the +// direction again). + +import { matchesDeclaredRoot } from './architecture-ownership.ts'; +import type { LayeringViolation, ResolvedImportEdge } from './model.ts'; + +const COMMANDS_SCHEMA_ROOT = 'src/commands/schema/'; +const COMMANDS_ROOT = 'src/commands/'; + +export function checkCommandsSchemaBoundary( + edges: readonly ResolvedImportEdge[], +): LayeringViolation[] { + return edges + .filter( + (edge) => + matchesDeclaredRoot(edge.file, COMMANDS_ROOT) && + !matchesDeclaredRoot(edge.file, COMMANDS_SCHEMA_ROOT) && + matchesDeclaredRoot(edge.target, COMMANDS_SCHEMA_ROOT), + ) + .map((edge) => ({ + rule: 'R2 commands-floor', + file: edge.file, + line: edge.line, + message: + `${edge.file} must not import ${edge.target}: commands/schema/ is the CLI/MCP schema ` + + 'layer that renders the command facets, so it sits above the rest of commands/ and ' + + 'reads them; commands must not import it back (#2543). The shared CommandSchema type ' + + 'and flag grammar live in @agent-device/command-registry, below both.', + })); +} diff --git a/scripts/layering/daemon-modularity.test.ts b/scripts/layering/daemon-modularity.test.ts index 121abd16f9..f0533ae2c7 100644 --- a/scripts/layering/daemon-modularity.test.ts +++ b/scripts/layering/daemon-modularity.test.ts @@ -106,38 +106,6 @@ test('R10 banks an R7 shrink with no edit anywhere', () => { ); }); -test('R10 forbids the daemon importing the CLI schema layer, value or type', () => { - const valueEdge: ResolvedImportEdge = { - ...importEdge('src/daemon/request-execution-scope.ts', 'src/cli-schema/command-schema.ts'), - typeOnly: false, - }; - const typeEdge = importEdge( - 'src/daemon/session-action-recorder.ts', - 'src/cli-schema/command-schema.ts', - ); - const violations = checkDaemonModularityRatchets( - [...baselineEdges(), valueEdge, typeEdge], - REFERENCE, - REFERENCE, - ); - assert.deepEqual( - violations.map(({ rule, file }) => ({ rule, file })), - [ - { rule: 'R10 daemon-modularity', file: 'src/daemon/request-execution-scope.ts' }, - { rule: 'R10 daemon-modularity', file: 'src/daemon/session-action-recorder.ts' }, - ], - ); - assert.match(violations[0]!.message, /must not import src\/cli-schema\/command-schema\.ts/); -}); - -test('R10 permits a non-daemon importer of the CLI schema layer', () => { - const cliEdge = importEdge('src/cli/parser/args.ts', 'src/cli-schema/command-schema.ts'); - assert.deepEqual( - checkDaemonModularityRatchets([...baselineEdges(), cliEdge], REFERENCE, REFERENCE), - [], - ); -}); - test('external daemon request/session-state importer membership changes require the baseline to change', () => { const edges = resolveImportEdges( new Map([ diff --git a/scripts/layering/daemon-modularity.ts b/scripts/layering/daemon-modularity.ts index 6366a34b5a..26d8ff08d5 100644 --- a/scripts/layering/daemon-modularity.ts +++ b/scripts/layering/daemon-modularity.ts @@ -65,35 +65,10 @@ export function checkDaemonModularityRatchets( ...checkSessionStateBaseline(measured.sessionState, reference.sessionState), ...checkTypeCycleBaseline(measured.largestTypeCycle, reference.largestTypeCycle), ...checkDaemonTypesImporters(edges), - ...checkDaemonCliSchemaBoundary(edges), ...checkLogicalModuleImports(edges), ]; } -// The daemon resolves command routes through the registry and fills request defaults through the -// command registry, so it never needs the CLI schema layer. #2543 cut the last two value edges -// (`request-execution-scope.ts` and `session-action-recorder.ts`); this pins that decoupling so a -// future daemon module cannot reach `src/cli-schema/` again and re-pull the parser closure. -const DAEMON_FORBIDDEN_CLI_SCHEMA_ROOT = 'src/cli-schema/'; - -function checkDaemonCliSchemaBoundary(edges: readonly ResolvedImportEdge[]): LayeringViolation[] { - return edges - .filter( - (edge) => - edge.file.startsWith('src/daemon/') && - matchesDeclaredRoot(edge.target, DAEMON_FORBIDDEN_CLI_SCHEMA_ROOT), - ) - .map((edge) => ({ - rule: 'R10 daemon-modularity', - file: edge.file, - line: edge.line, - message: - `${edge.file} must not import ${edge.target}: the daemon resolves routes through ` + - '@agent-device/command-registry and must not load the CLI schema layer. Declare the shared ' + - 'shape in the command registry or contracts, not in src/cli-schema/.', - })); -} - export function checkRetiredSessionLifecyclePaths( sourceFiles: readonly string[], ): LayeringViolation[] { diff --git a/scripts/layering/zone-policy.test.ts b/scripts/layering/zone-policy.test.ts index 315a44a4a2..3b999bc630 100644 --- a/scripts/layering/zone-policy.test.ts +++ b/scripts/layering/zone-policy.test.ts @@ -44,22 +44,6 @@ test('R2 commands-floor closes the remaining zones below the command surface, wh assert.deepEqual(firing(edge('src/mcp/tools.ts', 'mcp', 'commands')), []); }); -test('R2 commands-floor places commands below the cli-schema layer, whatever the kind', () => { - // #2543: cli-schema renders the facets commands authors, so it reads commands and commands never - // reads it back. The direction holds for value, type-only, and dynamic forms alike. - for (const kind of [{}, { typeOnly: true }, { dynamic: true }]) { - assert.ok( - firing(edge('src/commands/thing.ts', 'commands', 'cli-schema', kind)).includes( - 'R2 commands-floor', - ), - `commands -> cli-schema ${JSON.stringify(kind)} must violate R2`, - ); - } - - // The declared direction — cli-schema reading the facets — must stay silent. - assert.deepEqual(firing(edge('src/cli-schema/command-schema.ts', 'cli-schema', 'commands')), []); -}); - test('the retired R3 platforms seam has no zone-policy declaration', () => { assert.equal( ZONE_POLICIES.some(({ rule }) => rule === 'R3 platforms-seam'), diff --git a/scripts/layering/zone-policy.ts b/scripts/layering/zone-policy.ts index c14d48fd12..33ac259aba 100644 --- a/scripts/layering/zone-policy.ts +++ b/scripts/layering/zone-policy.ts @@ -44,10 +44,13 @@ export type ZonePolicy = { * packages/kernel, where package resolution and R11 package-boundaries enforce * the sink property physically — a package cannot import root src at all. * - * The two rows share `R2 commands-floor`: both state that commands sits at a floor and is read - * from above. The first keeps `core`/`daemon` below it; the second places `cli-schema` above it, - * so commands may not import the schema layer that renders its facets (#2543 declared the - * direction after the shared schema grammar moved down to the command registry). + * `R2 commands-floor` states that commands sits at a floor and is read from above, keeping + * `core`/`daemon` below it. It used to carry a second row placing `cli-schema` above commands + * (#2543: commands may not import the schema layer that renders its facets), but #2679 folded + * `cli-schema` into `commands/schema/`, putting both sides of that direction in the same zone — + * a same-zone edge this table can never see, since `checkLayeringRules` only evaluates edges + * that cross zones. `commands-schema-boundary.ts` enforces that direction now, as a + * folder-scoped rule instead of a zone-policy row. */ export const ZONE_POLICIES: readonly ZonePolicy[] = [ { @@ -58,15 +61,6 @@ export const ZONE_POLICIES: readonly ZonePolicy[] = [ 'commands/ is the command surface, above these zones. Depend on shared kernel/contracts ' + 'instead; if two zones need the same rule, put the rule below both of them.', }, - { - rule: 'R2 commands-floor', - from: ['commands'], - to: ['cli-schema'], - hint: - 'cli-schema is the CLI/MCP schema layer that renders the command facets, so it sits above ' + - 'commands and reads them; commands must not import it back. The shared CommandSchema type and ' + - 'flag grammar live in @agent-device/command-registry, below both zones.', - }, ]; function kindOf(imp: ImportEdge): 'type-only' | 'dynamic' | 'value' {