Skip to content

Commit 353d979

Browse files
committed
refactor: derive batch input schema
1 parent ddc9ea1 commit 353d979

2 files changed

Lines changed: 46 additions & 39 deletions

File tree

src/commands/semantic-batch.ts

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import { defineSemanticCommand, type JsonSchema } from './semantic-contract.ts';
44
import { prepareSemanticBatchStep, type SemanticDaemonCommand } from './semantic-grammar.ts';
55
import {
66
assertAllowedKeys,
7-
commandInputSchema,
87
commonToClientOptions,
9-
optionalEnum,
10-
optionalInteger,
11-
optionalString,
12-
readCommonInput,
13-
readInputRecord,
8+
customField,
9+
enumField,
10+
fieldsInputSchema,
11+
integerField,
12+
readFieldInput,
1413
requiredEnum,
15-
type CommonCommandInput,
14+
requiredField,
15+
stringField,
16+
type InferCommandInput,
17+
type SemanticFieldMap,
1618
} from './semantic-common.ts';
1719

18-
type BatchInput = CommonCommandInput & {
20+
type BatchInput = InferCommandInput<SemanticFieldMap> & {
1921
steps: BatchStep[];
2022
onError?: 'stop';
2123
maxSteps?: number;
@@ -25,32 +27,37 @@ type BatchInput = CommonCommandInput & {
2527
export function createBatchSemanticCommand<const TCommand extends SemanticDaemonCommand>(
2628
nestedCommands: readonly TCommand[],
2729
) {
30+
const fields = batchFields(nestedCommands);
2831
return defineSemanticCommand({
2932
name: 'batch',
3033
description: 'Run multiple structured command steps in one daemon request.',
31-
inputSchema: commandInputSchema(
32-
{
33-
steps: {
34+
inputSchema: fieldsInputSchema(fields),
35+
outputSchema: batchResultSchema(),
36+
readInput: (input) => readBatchInput(input, fields),
37+
run: (client, input) => client.batch.run(toBatchOptions(input)),
38+
});
39+
}
40+
41+
function batchFields(nestedCommands: readonly SemanticDaemonCommand[]) {
42+
return {
43+
steps: requiredField(
44+
customField<BatchStep[]>(
45+
{
3446
type: 'array',
3547
description:
3648
'Structured batch steps. CLI JSON parsing belongs to the CLI normalizer; MCP passes this array directly.',
3749
items: batchStepSchema(nestedCommands),
3850
},
39-
onError: { type: 'string', enum: ['stop'], description: 'Batch failure policy.' },
40-
maxSteps: {
41-
type: 'integer',
42-
minimum: 1,
43-
maximum: 1000,
44-
description: 'Maximum number of steps accepted for this batch.',
45-
},
46-
out: { type: 'string', description: 'Optional output path for command artifacts.' },
47-
},
48-
['steps'],
51+
(record, key) => readBatchSteps(record[key], nestedCommands),
52+
),
4953
),
50-
outputSchema: batchResultSchema(),
51-
readInput: (input) => readBatchInput(input, nestedCommands),
52-
run: (client, input) => client.batch.run(toBatchOptions(input)),
53-
});
54+
onError: enumField(['stop'] as const, 'Batch failure policy.'),
55+
maxSteps: integerField('Maximum number of steps accepted for this batch.', {
56+
min: 1,
57+
max: 1000,
58+
}),
59+
out: stringField('Optional output path for command artifacts.'),
60+
};
5461
}
5562

5663
function batchStepSchema(nestedCommands: readonly SemanticDaemonCommand[]): JsonSchema {
@@ -90,27 +97,20 @@ function batchResultSchema(): JsonSchema {
9097
};
9198
}
9299

93-
function readBatchInput(
94-
input: unknown,
95-
nestedCommands: readonly SemanticDaemonCommand[],
96-
): BatchInput {
97-
const record = readInputRecord(input);
98-
const maxSteps = optionalInteger(record, 'maxSteps', { min: 1, max: 1000 });
100+
function readBatchInput(input: unknown, fields: ReturnType<typeof batchFields>): BatchInput {
101+
const parsed = readFieldInput(input, fields);
99102
const normalized = validateAndNormalizeBatchSteps(
100-
readBatchSteps(record.steps, nestedCommands),
101-
maxSteps ?? DEFAULT_BATCH_MAX_STEPS,
103+
parsed.steps,
104+
parsed.maxSteps ?? DEFAULT_BATCH_MAX_STEPS,
102105
);
103106
return {
104-
...readCommonInput(record),
107+
...parsed,
105108
steps: normalized.map(({ command, positionals, flags, runtime }) => ({
106109
command,
107110
positionals,
108111
flags,
109112
runtime,
110113
})),
111-
onError: optionalEnum(record, 'onError', ['stop'] as const),
112-
maxSteps,
113-
out: optionalString(record, 'out'),
114114
};
115115
}
116116

src/commands/semantic-common.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export type SelectorSnapshotInput = {
4444

4545
export type PointInput = { x: number; y: number };
4646

47-
export function commandInputSchema(
47+
function commandInputSchema(
4848
properties: Record<string, JsonSchema>,
4949
required: readonly string[] = [],
5050
): JsonSchema {
@@ -187,6 +187,13 @@ export function jsonSchemaField<T>(schema: JsonSchema): SemanticField<T> {
187187
return optionalField(schema, (record, key) => record[key] as T | undefined);
188188
}
189189

190+
export function customField<T>(
191+
schema: JsonSchema,
192+
read: (record: Record<string, unknown>, key: string) => T | undefined,
193+
): SemanticField<T> {
194+
return optionalField(schema, read);
195+
}
196+
190197
export function interactionTargetField(): SemanticField<SemanticInteractionTarget> {
191198
return optionalField(interactionTargetSchema(), (record, key) =>
192199
record[key] === undefined ? undefined : readInteractionTarget(record, key),
@@ -308,7 +315,7 @@ function requiredString(record: Record<string, unknown>, key: string): string {
308315
return value;
309316
}
310317

311-
export function optionalString(record: Record<string, unknown>, key: string): string | undefined {
318+
function optionalString(record: Record<string, unknown>, key: string): string | undefined {
312319
const value = record[key];
313320
if (value === undefined) return undefined;
314321
if (typeof value !== 'string' || value.length === 0) {

0 commit comments

Comments
 (0)