Skip to content

Commit a04c376

Browse files
committed
refactor(mcp): project recording family output schemas from their owning module
Moves the 2 hand-authored MCP outputSchema entries for record and trace out of command-output-schemas.ts into a frozen RECORDING_COMMAND_OUTPUT_SCHEMAS map exported from src/commands/recording/index.ts, following the #2810 projection seam already used by the replay, system, interaction, device-management, and push-management families on this stack. Registers the family in the PROJECTED_FAMILIES disjointness check and extends the pre-existing command-tools-recording-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. Neither command carries the post-action observation trait, so no settle-graft copy applies here. The moved code is byte-identical to what command-output-schemas.ts held: same objectSchema/stringSchema/enumSchema calls and field order, with a locally duplicated constSchema helper and artifactSchema object matching the precedent from the other families' moves (artifactSchema had no other consumer in command-output-schemas.ts once record/trace moved, so it moved outright rather than staying duplicated). Part of #2819.
1 parent c7d92a6 commit a04c376

4 files changed

Lines changed: 130 additions & 77 deletions

File tree

‎src/commands/recording/index.ts‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,121 @@ import {
77
RECORDING_EXPORT_QUALITIES,
88
RECORDING_SCOPE_VALUES,
99
} from '@agent-device/contracts/recording';
10+
import { NATIVE_PATH_DISPOSITION_VALUES } from '@agent-device/contracts/recording-native-path';
11+
import { RECORDER_OBSERVATION_VALUES } from '@agent-device/contracts/recording-stop-observation';
1012
import { AppError } from '@agent-device/kernel/errors';
1113
import type { CommandSchemaOverride } from '@agent-device/command-registry/command-schema';
14+
import type { CommandResultMap } from '@agent-device/command-registry/command-result';
1215
import type { FlagKey } from '@agent-device/command-registry/flag-types';
1316
import { commonInputFromFlags, direct, optionalString } from '../cli-grammar/common.ts';
1417
import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts';
18+
import type { JsonSchema } from '../command-contract.ts';
1519
import {
1620
booleanField,
21+
booleanSchema,
1722
enumField,
23+
enumSchema,
1824
integerField,
25+
numberSchema,
26+
objectSchema,
27+
looseObjectSchema,
1928
requiredField,
2029
retiredField,
2130
stringField,
31+
stringSchema,
2232
} from '../command-input.ts';
2333
import { defineCommandFacet, defineCommandFamilyFromFacets } from '../family/types.ts';
2434
import { defineFieldCommandMetadata } from '../field-command-contract.ts';
2535
import { recordingCliOutputFormatters } from './output.ts';
2636

37+
function constSchema(value: string): JsonSchema {
38+
return { type: 'string', const: value };
39+
}
40+
41+
const artifactSchema = objectSchema(
42+
{
43+
field: stringSchema(),
44+
artifactType: stringSchema(),
45+
path: stringSchema(),
46+
localPath: stringSchema(),
47+
fileName: stringSchema(),
48+
},
49+
['field'],
50+
);
51+
52+
/**
53+
* This family's advertised MCP `outputSchema`s, keyed by daemon command name and projected into
54+
* the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no
55+
* `additionalProperties: false`, so additive response fields keep validating. Neither command
56+
* carries the post-action observation trait (#1652): both fire-and-report a recording or trace
57+
* lifecycle change, not an interaction, so no settle-graft copy applies here.
58+
*/
59+
export const RECORDING_COMMAND_OUTPUT_SCHEMAS = {
60+
// packages/contracts/src/recording.ts
61+
record: {
62+
type: 'object',
63+
oneOf: [
64+
objectSchema(
65+
{
66+
recording: constSchema('started'),
67+
outPath: stringSchema(),
68+
sessionStateDir: stringSchema(),
69+
recordingBackend: stringSchema(),
70+
recordingScope: stringSchema(),
71+
recordOnlySession: booleanSchema(),
72+
activeSessionApp: looseObjectSchema(),
73+
showTouches: booleanSchema(),
74+
},
75+
['recording', 'outPath', 'sessionStateDir', 'showTouches'],
76+
),
77+
objectSchema(
78+
{
79+
recording: constSchema('stopped'),
80+
outPath: stringSchema(),
81+
telemetryPath: stringSchema(),
82+
artifacts: { type: 'array', items: artifactSchema },
83+
recordingBackend: stringSchema(),
84+
recordingScope: stringSchema(),
85+
recordOnlySession: booleanSchema(),
86+
activeSessionApp: looseObjectSchema(),
87+
durationMs: numberSchema(),
88+
capturedDurationMs: numberSchema(),
89+
recorder: enumSchema(
90+
RECORDER_OBSERVATION_VALUES,
91+
'What the recorder was observed doing when the recording was stopped: confirmed, or lost when the session holding it died. ADR 0024 reserves unconfirmed for the step that gains the probe.',
92+
),
93+
nativePathDisposition: enumSchema(
94+
NATIVE_PATH_DISPOSITION_VALUES,
95+
'What became of the artifact path the recorder writes to: retirable while it still owes a removal, retired once that removal was verified. ADR 0024 reserves pending.',
96+
),
97+
showTouches: booleanSchema(),
98+
warning: stringSchema(),
99+
overlayWarning: stringSchema(),
100+
chunks: { type: 'array', items: looseObjectSchema() },
101+
},
102+
['recording', 'outPath', 'artifacts', 'durationMs', 'showTouches'],
103+
),
104+
],
105+
},
106+
trace: {
107+
type: 'object',
108+
oneOf: [
109+
objectSchema({ trace: constSchema('started'), outPath: stringSchema() }, [
110+
'trace',
111+
'outPath',
112+
]),
113+
objectSchema(
114+
{
115+
trace: constSchema('stopped'),
116+
outPath: stringSchema(),
117+
artifacts: { type: 'array', items: artifactSchema },
118+
},
119+
['trace', 'outPath', 'artifacts'],
120+
),
121+
],
122+
},
123+
} satisfies Pick<Record<keyof CommandResultMap, JsonSchema>, 'record' | 'trace'>;
124+
27125
const RECORD_COMMAND_NAME = 'record';
28126
const TRACE_COMMAND_NAME = 'trace';
29127
const RECORDING_ACTION_VALUES = ['start', 'stop'] as const;

‎src/mcp/__tests__/command-tools-recording-schemas.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
11
import assert from 'node:assert/strict';
22
import { test } from 'vitest';
3+
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
4+
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../../commands/recording/index.ts';
35
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
46
import { validateAgainstSchema } from './output-schema-validator.ts';
57

8+
const RECORDING_COMMANDS = Object.keys(RECORDING_COMMAND_OUTPUT_SCHEMAS) as Array<
9+
keyof typeof RECORDING_COMMAND_OUTPUT_SCHEMAS
10+
>;
11+
12+
// Neither record nor trace carries the post-action observation trait (#1652): both fire-and-report
13+
// a recording or trace lifecycle change, not an interaction, so the composed map never copies
14+
// either entry and reference equality must hold for both.
15+
test('MCP recording family output schemas are the family module entries, not copies', () => {
16+
for (const command of RECORDING_COMMANDS) {
17+
assert.equal(
18+
COMMAND_OUTPUT_SCHEMAS[command],
19+
RECORDING_COMMAND_OUTPUT_SCHEMAS[command],
20+
`${command} is not reference-equal to the recording module's own schema object`,
21+
);
22+
}
23+
});
24+
25+
test('the projected family map declares exactly the commands its module owns', () => {
26+
for (const command of RECORDING_COMMANDS) {
27+
assert.ok(
28+
ownerFilesForCommand(command).includes('src/commands/recording/index.ts'),
29+
`${command} projects its output schema from this module but does not name it as its owner`,
30+
);
31+
}
32+
});
33+
634
// `record stop` answers two independent questions (ADR 0024): whether a playable export exists, and
735
// whether its recorder stopped. Only the second one's words are additive properties here — the
836
// stopped branch's required list is what it was before either fact was reported.

‎src/mcp/__tests__/command-tools-replay-schemas.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { commandSupportsSettleObservation } from '@agent-device/command-registry
66
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts';
77
import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/push.ts';
88
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../../commands/interaction/index.ts';
9+
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../../commands/recording/index.ts';
910
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../../commands/replay/index.ts';
1011
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../../commands/system/index.ts';
1112
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
@@ -77,6 +78,7 @@ const PROJECTED_FAMILIES = [
7778
{ name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS },
7879
{ name: 'management-device', schemas: DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
7980
{ name: 'management-push', schemas: PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
81+
{ name: 'recording', schemas: RECORDING_COMMAND_OUTPUT_SCHEMAS },
8082
] as const;
8183

8284
test('projected output-schema families claim disjoint commands and survive the composition intact', () => {

‎src/mcp/command-output-schemas.ts‎

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import {
1313
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/device.ts';
1414
import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/push.ts';
1515
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../commands/interaction/index.ts';
16+
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../commands/recording/index.ts';
1617
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../commands/replay/index.ts';
1718
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../commands/system/index.ts';
18-
import { NATIVE_PATH_DISPOSITION_VALUES } from '@agent-device/contracts/recording-native-path';
19-
import { RECORDER_OBSERVATION_VALUES } from '@agent-device/contracts/recording-stop-observation';
2019
import { DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device';
2120

2221
/**
@@ -54,17 +53,6 @@ function constSchema(value: string): JsonSchema {
5453
return { type: 'string', const: value };
5554
}
5655

57-
const artifactSchema = objectSchema(
58-
{
59-
field: stringSchema(),
60-
artifactType: stringSchema(),
61-
path: stringSchema(),
62-
localPath: stringSchema(),
63-
fileName: stringSchema(),
64-
},
65-
['field'],
66-
);
67-
6856
// PostActionSurfaceChange (packages/contracts/src/interaction.ts) — the post-action capture
6957
// describes a different surface than the pre-action baseline (#2438), so no same-surface
7058
// comparison is presented across it. Also mirrored in the interaction family module
@@ -347,77 +335,14 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
347335
['mode', 'baselineInitialized', 'summary', 'lines'],
348336
),
349337

350-
// packages/contracts/src/recording.ts
351-
record: {
352-
type: 'object',
353-
oneOf: [
354-
objectSchema(
355-
{
356-
recording: constSchema('started'),
357-
outPath: stringSchema(),
358-
sessionStateDir: stringSchema(),
359-
recordingBackend: stringSchema(),
360-
recordingScope: stringSchema(),
361-
recordOnlySession: booleanSchema(),
362-
activeSessionApp: looseObjectSchema(),
363-
showTouches: booleanSchema(),
364-
},
365-
['recording', 'outPath', 'sessionStateDir', 'showTouches'],
366-
),
367-
objectSchema(
368-
{
369-
recording: constSchema('stopped'),
370-
outPath: stringSchema(),
371-
telemetryPath: stringSchema(),
372-
artifacts: { type: 'array', items: artifactSchema },
373-
recordingBackend: stringSchema(),
374-
recordingScope: stringSchema(),
375-
recordOnlySession: booleanSchema(),
376-
activeSessionApp: looseObjectSchema(),
377-
durationMs: numberSchema(),
378-
capturedDurationMs: numberSchema(),
379-
recorder: enumSchema(
380-
RECORDER_OBSERVATION_VALUES,
381-
'What the recorder was observed doing when the recording was stopped: confirmed, or lost when the session holding it died. ADR 0024 reserves unconfirmed for the step that gains the probe.',
382-
),
383-
nativePathDisposition: enumSchema(
384-
NATIVE_PATH_DISPOSITION_VALUES,
385-
'What became of the artifact path the recorder writes to: retirable while it still owes a removal, retired once that removal was verified. ADR 0024 reserves pending.',
386-
),
387-
showTouches: booleanSchema(),
388-
warning: stringSchema(),
389-
overlayWarning: stringSchema(),
390-
chunks: { type: 'array', items: looseObjectSchema() },
391-
},
392-
['recording', 'outPath', 'artifacts', 'durationMs', 'showTouches'],
393-
),
394-
],
395-
},
396-
trace: {
397-
type: 'object',
398-
oneOf: [
399-
objectSchema({ trace: constSchema('started'), outPath: stringSchema() }, [
400-
'trace',
401-
'outPath',
402-
]),
403-
objectSchema(
404-
{
405-
trace: constSchema('stopped'),
406-
outPath: stringSchema(),
407-
artifacts: { type: 'array', items: artifactSchema },
408-
},
409-
['trace', 'outPath', 'artifacts'],
410-
),
411-
],
412-
},
413-
414338
// A family that owns its commands authors their advertised response shape beside the
415339
// command surface and projects it here. This spread stays last: a hand-written entry for
416340
// a projected command then fails as TS2783 instead of quietly overriding the family's,
417341
// and this map's `satisfies` still refuses a missing `CommandResultMap` key.
418342
...DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
419343
...PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
420344
...INTERACTION_COMMAND_OUTPUT_SCHEMAS,
345+
...RECORDING_COMMAND_OUTPUT_SCHEMAS,
421346
...REPLAY_COMMAND_OUTPUT_SCHEMAS,
422347
...SYSTEM_COMMAND_OUTPUT_SCHEMAS,
423348
} satisfies Record<keyof CommandResultMap, JsonSchema>;

0 commit comments

Comments
 (0)