From bb0cc29eb31604fb10ede93f4a09704111ad76b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 28 Sep 2026 15:29:44 +0200 Subject: [PATCH] refactor(mcp): project diff family output schema from its owning module Moves the single hand-authored MCP outputSchema entry for diff out of command-output-schemas.ts into a frozen DIFF_COMMAND_OUTPUT_SCHEMAS map exported from src/commands/capture/diff.ts, following the #2819 projection seam already used by wait and the other migrated families on this stack. Registers the family in the PROJECTED_FAMILIES disjointness check and adds command-tools-capture-diff-schemas.test.ts with a reference-equality proof against the module object and an owner-file check, matching the pattern from the other projected families. diff does not carry the post-action observation trait (#1652): it reports a snapshot comparison, not an interaction, so no settle-graft copy applies here. The moved code is byte-identical to what command-output-schemas.ts held: same objectSchema/numberSchema/enumSchema/stringSchema/stringArraySchema call and field order, and the shared command-input.ts helpers are unchanged from origin/main, so the published outputSchema for diff stays byte-identical to origin/main. Part of #2819. --- src/commands/capture/diff.ts | 47 +++++++++++++++++++ ...command-tools-capture-diff-schemas.test.ts | 40 ++++++++++++++++ .../command-tools-replay-schemas.test.ts | 2 + src/mcp/command-output-schemas.ts | 33 +------------ 4 files changed, 91 insertions(+), 31 deletions(-) create mode 100644 src/mcp/__tests__/command-tools-capture-diff-schemas.test.ts diff --git a/src/commands/capture/diff.ts b/src/commands/capture/diff.ts index 20c9194e79..3eee927c46 100644 --- a/src/commands/capture/diff.ts +++ b/src/commands/capture/diff.ts @@ -1,20 +1,67 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { SNAPSHOT_FLAGS } from '@agent-device/command-registry/flag-groups'; +import type { CommandResultMap } from '@agent-device/command-registry/command-result'; import { AppError } from '@agent-device/kernel/errors'; import { booleanField, + booleanSchema, + constSchema, + enumSchema, integerField, jsonSchemaField, + numberSchema, + objectSchema, requiredField, + stringArraySchema, stringField, + stringSchema, } from '../command-input.ts'; import { commonInputFromFlags, direct, requiredDaemonString } from '../cli-grammar/common.ts'; import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts'; +import type { JsonSchema } from '../command-contract.ts'; import { defineCommandFacet } from '../family/types.ts'; import { defineFieldCommandMetadata } from '../field-command-contract.ts'; const DIFF_COMMAND_NAME = 'diff'; +/** + * This family's advertised MCP `outputSchema`, keyed by daemon command name and projected into + * the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no + * `additionalProperties: false`, so additive response fields keep validating. `diff` does not + * carry the post-action observation trait (#1652): it reports a snapshot comparison, not an + * interaction, so no settle-graft copy applies here. + */ +export const DIFF_COMMAND_OUTPUT_SCHEMAS = { + // packages/contracts/src/diff.ts — the public Node command accepts snapshot diffs. + diff: objectSchema( + { + mode: constSchema('snapshot'), + baselineInitialized: booleanSchema(), + summary: objectSchema( + { + additions: numberSchema(), + removals: numberSchema(), + unchanged: numberSchema(), + }, + ['additions', 'removals', 'unchanged'], + ), + lines: { + type: 'array', + items: objectSchema( + { + kind: enumSchema(['added', 'removed', 'unchanged']), + text: stringSchema(), + ref: stringSchema(), + }, + ['kind', 'text'], + ), + }, + warnings: stringArraySchema(), + }, + ['mode', 'baselineInitialized', 'summary', 'lines'], + ), +} satisfies Pick, 'diff'>; + const diffCommandDescription = 'Compare accessibility snapshots or screenshots to identify UI changes. Use snapshot comparisons for semantic tree changes and screenshot comparisons for pixel differences.'; diff --git a/src/mcp/__tests__/command-tools-capture-diff-schemas.test.ts b/src/mcp/__tests__/command-tools-capture-diff-schemas.test.ts new file mode 100644 index 0000000000..4f7be24ba6 --- /dev/null +++ b/src/mcp/__tests__/command-tools-capture-diff-schemas.test.ts @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; +import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/diff.ts'; +import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts'; + +const DIFF_COMMANDS = Object.keys(DIFF_COMMAND_OUTPUT_SCHEMAS) as Array< + keyof typeof DIFF_COMMAND_OUTPUT_SCHEMAS +>; + +// diff does not carry the post-action observation trait (#1652): it reports a snapshot +// comparison, not an interaction, so the composed map never copies the entry and reference +// equality must hold. +test('MCP diff family output schemas are the family module entries, not copies', () => { + for (const command of DIFF_COMMANDS) { + assert.equal( + COMMAND_OUTPUT_SCHEMAS[command], + DIFF_COMMAND_OUTPUT_SCHEMAS[command], + `${command} is not reference-equal to the diff module's own schema object`, + ); + } +}); + +test('the projected family map declares exactly the commands its module owns', () => { + for (const command of DIFF_COMMANDS) { + assert.ok( + ownerFilesForCommand(command).includes('src/commands/capture/diff.ts'), + `${command} projects its output schema from this module but does not name it as its owner`, + ); + } +}); + +test('the diff line kinds include unchanged context lines', () => { + const lines = ( + DIFF_COMMAND_OUTPUT_SCHEMAS.diff as unknown as { + properties: { lines: { items: { properties: { kind: { enum: string[] } } } } }; + } + ).properties.lines.items.properties.kind; + assert.deepEqual(lines.enum, ['added', 'removed', 'unchanged']); +}); diff --git a/src/mcp/__tests__/command-tools-replay-schemas.test.ts b/src/mcp/__tests__/command-tools-replay-schemas.test.ts index b7054bfdac..aded93dd40 100644 --- a/src/mcp/__tests__/command-tools-replay-schemas.test.ts +++ b/src/mcp/__tests__/command-tools-replay-schemas.test.ts @@ -3,6 +3,7 @@ import { test } from 'vitest'; import type { ReplayCommandResult, ReplaySuiteResult } from '@agent-device/contracts/replay'; import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry'; +import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/diff.ts'; import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/wait.ts'; import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts'; import { DOCTOR_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/doctor.ts'; @@ -80,6 +81,7 @@ const PROJECTED_FAMILIES = [ { name: 'replay', schemas: REPLAY_COMMAND_OUTPUT_SCHEMAS }, { name: 'system', schemas: SYSTEM_COMMAND_OUTPUT_SCHEMAS }, { name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS }, + { name: 'capture-diff', schemas: DIFF_COMMAND_OUTPUT_SCHEMAS }, { name: 'capture-wait', schemas: WAIT_COMMAND_OUTPUT_SCHEMAS }, { name: 'management-prepare', schemas: PREPARE_COMMAND_OUTPUT_SCHEMAS }, { name: 'management-doctor', schemas: DOCTOR_COMMAND_OUTPUT_SCHEMAS }, diff --git a/src/mcp/command-output-schemas.ts b/src/mcp/command-output-schemas.ts index 60665f5d71..1acddb56d6 100644 --- a/src/mcp/command-output-schemas.ts +++ b/src/mcp/command-output-schemas.ts @@ -3,13 +3,12 @@ import type { CommandResultMap } from '@agent-device/command-registry/command-re import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry'; import { booleanSchema, - constSchema, enumSchema, numberSchema, objectSchema, - stringArraySchema, stringSchema, } from '../commands/command-input.ts'; +import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/diff.ts'; import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/wait.ts'; import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/prepare.ts'; import { DOCTOR_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/doctor.ts'; @@ -183,39 +182,11 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = { ['direction'], ), - // packages/contracts/src/diff.ts — the public Node command accepts snapshot diffs. - diff: objectSchema( - { - mode: constSchema('snapshot'), - baselineInitialized: booleanSchema(), - summary: objectSchema( - { - additions: numberSchema(), - removals: numberSchema(), - unchanged: numberSchema(), - }, - ['additions', 'removals', 'unchanged'], - ), - lines: { - type: 'array', - items: objectSchema( - { - kind: enumSchema(['added', 'removed']), - text: stringSchema(), - ref: stringSchema(), - }, - ['kind', 'text'], - ), - }, - warnings: stringArraySchema(), - }, - ['mode', 'baselineInitialized', 'summary', 'lines'], - ), - // A family that owns its commands authors their advertised response shape beside the // command surface and projects it here. This spread stays last: a hand-written entry for // a projected command then fails as TS2783 instead of quietly overriding the family's, // and this map's `satisfies` still refuses a missing `CommandResultMap` key. + ...DIFF_COMMAND_OUTPUT_SCHEMAS, ...WAIT_COMMAND_OUTPUT_SCHEMAS, ...PREPARE_COMMAND_OUTPUT_SCHEMAS, ...DOCTOR_COMMAND_OUTPUT_SCHEMAS,