@cleocode/contracts 2026.5.121 → 2026.5.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/__tests__/completion-operation-contracts.test.d.ts +8 -0
  2. package/dist/__tests__/completion-operation-contracts.test.d.ts.map +1 -0
  3. package/dist/__tests__/completion-operation-contracts.test.js +114 -0
  4. package/dist/__tests__/completion-operation-contracts.test.js.map +1 -0
  5. package/dist/__tests__/operation-envelope-validation.test.d.ts +8 -0
  6. package/dist/__tests__/operation-envelope-validation.test.d.ts.map +1 -0
  7. package/dist/__tests__/operation-envelope-validation.test.js +105 -0
  8. package/dist/__tests__/operation-envelope-validation.test.js.map +1 -0
  9. package/dist/__tests__/satisfies-atom.test.d.ts +24 -0
  10. package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
  11. package/dist/__tests__/satisfies-atom.test.js +320 -0
  12. package/dist/__tests__/satisfies-atom.test.js.map +1 -0
  13. package/dist/__tests__/validator-types.test.d.ts +13 -0
  14. package/dist/__tests__/validator-types.test.d.ts.map +1 -0
  15. package/dist/__tests__/validator-types.test.js +293 -0
  16. package/dist/__tests__/validator-types.test.js.map +1 -0
  17. package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts +8 -0
  18. package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts.map +1 -0
  19. package/dist/__tests__/workgraph-context-scaffold-contracts.test.js +188 -0
  20. package/dist/__tests__/workgraph-context-scaffold-contracts.test.js.map +1 -0
  21. package/dist/__tests__/workgraph-operation-contracts.test.d.ts +8 -0
  22. package/dist/__tests__/workgraph-operation-contracts.test.d.ts.map +1 -0
  23. package/dist/__tests__/workgraph-operation-contracts.test.js +115 -0
  24. package/dist/__tests__/workgraph-operation-contracts.test.js.map +1 -0
  25. package/dist/config.d.ts +56 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/data-accessor.d.ts +194 -0
  28. package/dist/data-accessor.d.ts.map +1 -1
  29. package/dist/data-accessor.js.map +1 -1
  30. package/dist/dispatch/__tests__/docs-update-registry.test.d.ts +2 -0
  31. package/dist/dispatch/__tests__/docs-update-registry.test.d.ts.map +1 -0
  32. package/dist/dispatch/__tests__/docs-update-registry.test.js +27 -0
  33. package/dist/dispatch/__tests__/docs-update-registry.test.js.map +1 -0
  34. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts +2 -0
  35. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts.map +1 -0
  36. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js +265 -0
  37. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js.map +1 -0
  38. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  39. package/dist/dispatch/operations-registry.js +487 -7
  40. package/dist/dispatch/operations-registry.js.map +1 -1
  41. package/dist/evidence-atom-schema.d.ts +91 -0
  42. package/dist/evidence-atom-schema.d.ts.map +1 -1
  43. package/dist/evidence-atom-schema.js +162 -1
  44. package/dist/evidence-atom-schema.js.map +1 -1
  45. package/dist/exit-codes.d.ts +41 -1
  46. package/dist/exit-codes.d.ts.map +1 -1
  47. package/dist/exit-codes.js +40 -0
  48. package/dist/exit-codes.js.map +1 -1
  49. package/dist/index.d.ts +17 -9
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +12 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/operation-envelope-validation.d.ts +79 -0
  54. package/dist/operation-envelope-validation.d.ts.map +1 -0
  55. package/dist/operation-envelope-validation.js +219 -0
  56. package/dist/operation-envelope-validation.js.map +1 -0
  57. package/dist/operations/__tests__/agent-operation-contract.test.d.ts +2 -0
  58. package/dist/operations/__tests__/agent-operation-contract.test.d.ts.map +1 -0
  59. package/dist/operations/__tests__/agent-operation-contract.test.js +128 -0
  60. package/dist/operations/__tests__/agent-operation-contract.test.js.map +1 -0
  61. package/dist/operations/__tests__/tasks-input-contracts.test.js +15 -0
  62. package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -1
  63. package/dist/operations/agent-operation-contract.d.ts +227 -0
  64. package/dist/operations/agent-operation-contract.d.ts.map +1 -0
  65. package/dist/operations/agent-operation-contract.js +38 -0
  66. package/dist/operations/agent-operation-contract.js.map +1 -0
  67. package/dist/operations/docs.d.ts +19 -1
  68. package/dist/operations/docs.d.ts.map +1 -1
  69. package/dist/operations/docs.js +20 -0
  70. package/dist/operations/docs.js.map +1 -1
  71. package/dist/operations/focus.d.ts +1 -1
  72. package/dist/operations/index.d.ts +1 -0
  73. package/dist/operations/index.d.ts.map +1 -1
  74. package/dist/operations/index.js +1 -0
  75. package/dist/operations/index.js.map +1 -1
  76. package/dist/operations/orchestrate.d.ts +66 -0
  77. package/dist/operations/orchestrate.d.ts.map +1 -1
  78. package/dist/operations/tasks.d.ts +617 -11
  79. package/dist/operations/tasks.d.ts.map +1 -1
  80. package/dist/operations/tasks.js +22 -3
  81. package/dist/operations/tasks.js.map +1 -1
  82. package/dist/task-record.d.ts +15 -0
  83. package/dist/task-record.d.ts.map +1 -1
  84. package/dist/task.d.ts +52 -5
  85. package/dist/task.d.ts.map +1 -1
  86. package/dist/task.js.map +1 -1
  87. package/dist/tasks.d.ts +944 -0
  88. package/dist/tasks.d.ts.map +1 -1
  89. package/dist/tasks.js +198 -1
  90. package/dist/tasks.js.map +1 -1
  91. package/dist/validator/index.d.ts +375 -0
  92. package/dist/validator/index.d.ts.map +1 -0
  93. package/dist/validator/index.js +162 -0
  94. package/dist/validator/index.js.map +1 -0
  95. package/dist/workgraph.d.ts +1950 -0
  96. package/dist/workgraph.d.ts.map +1 -0
  97. package/dist/workgraph.js +449 -0
  98. package/dist/workgraph.js.map +1 -0
  99. package/package.json +10 -2
  100. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  101. package/src/__tests__/completion-operation-contracts.test.ts +154 -0
  102. package/src/__tests__/operation-envelope-validation.test.ts +140 -0
  103. package/src/__tests__/satisfies-atom.test.ts +383 -0
  104. package/src/__tests__/validator-types.test.ts +359 -0
  105. package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
  106. package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
  107. package/src/config.ts +58 -0
  108. package/src/data-accessor.ts +213 -0
  109. package/src/dispatch/__tests__/docs-update-registry.test.ts +35 -0
  110. package/src/dispatch/__tests__/operation-envelope-snapshots.test.ts +283 -0
  111. package/src/dispatch/operations-registry.ts +502 -7
  112. package/src/evidence-atom-schema.ts +188 -1
  113. package/src/exit-codes.ts +42 -0
  114. package/src/index.ts +230 -2
  115. package/src/operation-envelope-validation.ts +319 -0
  116. package/src/operations/__tests__/agent-operation-contract.test.ts +161 -0
  117. package/src/operations/__tests__/tasks-input-contracts.test.ts +30 -0
  118. package/src/operations/agent-operation-contract.ts +253 -0
  119. package/src/operations/docs.ts +27 -7
  120. package/src/operations/focus.ts +1 -1
  121. package/src/operations/index.ts +1 -0
  122. package/src/operations/orchestrate.ts +71 -0
  123. package/src/operations/tasks.ts +662 -15
  124. package/src/task-record.ts +16 -0
  125. package/src/task.ts +54 -5
  126. package/src/tasks.ts +558 -0
  127. package/src/validator/index.ts +385 -0
  128. package/src/workgraph.ts +1153 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Operation-aware LAFS envelope validation.
3
+ *
4
+ * The base LAFS shape proves that an envelope is structurally valid. This
5
+ * module adds the dispatch-layer contract checks that require `_meta.operation`
6
+ * to resolve to a registered CLEO operation and, when a result schema is known,
7
+ * validates successful payloads against that operation's result contract.
8
+ *
9
+ * @task T10610
10
+ * @saga T10538
11
+ */
12
+
13
+ import type { OperationDef } from './dispatch/operation-def.js';
14
+ import { OPERATIONS } from './dispatch/operations-registry.js';
15
+ import type { LAFSEnvelope, LAFSErrorCategory, LAFSTransport, MVILevel } from './lafs.js';
16
+ import {
17
+ tasksFrontierResultSchema,
18
+ tasksRollupResultSchema,
19
+ tasksTraverseResultSchema,
20
+ tasksTreeResultSchema,
21
+ tasksWorkGraphAuditResultSchema,
22
+ } from './workgraph.js';
23
+
24
+ interface SafeParseIssue {
25
+ readonly path: readonly PropertyKey[];
26
+ readonly message: string;
27
+ readonly code?: string;
28
+ }
29
+
30
+ interface SafeParseError {
31
+ readonly issues: readonly SafeParseIssue[];
32
+ }
33
+
34
+ interface ResultSchema {
35
+ safeParse(
36
+ input: unknown,
37
+ ): { success: true; data: unknown } | { success: false; error: SafeParseError };
38
+ }
39
+
40
+ /** Stable issue code emitted when `_meta.operation` is absent or unknown. */
41
+ export const E_LAFS_OPERATION_UNREGISTERED = 'E_LAFS_OPERATION_UNREGISTERED';
42
+
43
+ /** Stable issue code emitted when an operation result fails its contract schema. */
44
+ export const E_LAFS_OPERATION_RESULT_SCHEMA = 'E_LAFS_OPERATION_RESULT_SCHEMA';
45
+
46
+ /** Stable issue code emitted when success/error envelope invariants are violated. */
47
+ export const E_LAFS_OPERATION_ERROR_SHAPE = 'E_LAFS_OPERATION_ERROR_SHAPE';
48
+
49
+ /** Dot-delimited operation name, e.g. `tasks.frontier`. */
50
+ export type OperationEnvelopeName = `${string}.${string}`;
51
+
52
+ /** One issue produced by operation-aware envelope validation. */
53
+ export interface OperationEnvelopeValidationIssue {
54
+ /** JSON Pointer path into the envelope. */
55
+ readonly path: string;
56
+ /** Stable machine-readable issue code. */
57
+ readonly code: string;
58
+ /** Human-readable validation failure. */
59
+ readonly message: string;
60
+ /** Optional underlying schema keyword or parser code. */
61
+ readonly keyword?: string;
62
+ }
63
+
64
+ /** Result of validating a LAFS envelope against the dispatch operation registry. */
65
+ export interface OperationEnvelopeValidationResult {
66
+ /** True only when base LAFS shape, registry lookup, and result schema checks pass. */
67
+ readonly valid: boolean;
68
+ /** Base LAFS structural verdict. */
69
+ readonly envelopeValid: boolean;
70
+ /** Resolved operation definition, if `_meta.operation` matched the registry. */
71
+ readonly operation?: OperationDef;
72
+ /** Result schema key that was enforced, when available. */
73
+ readonly resultSchemaOperation?: OperationEnvelopeName;
74
+ /** Structured issue list, including base LAFS and operation-aware issues. */
75
+ readonly issues: readonly OperationEnvelopeValidationIssue[];
76
+ /** Human-readable issue messages for CLI/reporting callers. */
77
+ readonly errors: readonly string[];
78
+ }
79
+
80
+ /** Options for operation-aware LAFS validation. */
81
+ export interface OperationEnvelopeValidationOptions {
82
+ /** Registry to resolve `_meta.operation` against. Defaults to canonical OPERATIONS. */
83
+ readonly operations?: readonly OperationDef[];
84
+ /** Per-operation result schemas. Defaults to the currently contracted WorkGraph reads. */
85
+ readonly resultSchemas?: ReadonlyMap<OperationEnvelopeName, ResultSchema>;
86
+ }
87
+
88
+ /** Canonical result schemas currently promoted to operation-aware LAFS validation. */
89
+ export const OPERATION_RESULT_SCHEMAS: ReadonlyMap<OperationEnvelopeName, ResultSchema> = new Map<
90
+ OperationEnvelopeName,
91
+ ResultSchema
92
+ >([
93
+ ['tasks.traverse', tasksTraverseResultSchema as ResultSchema],
94
+ ['tasks.tree', tasksTreeResultSchema as ResultSchema],
95
+ ['tasks.rollup', tasksRollupResultSchema as ResultSchema],
96
+ ['tasks.frontier', tasksFrontierResultSchema as ResultSchema],
97
+ ['tasks.workgraph.audit', tasksWorkGraphAuditResultSchema as ResultSchema],
98
+ ]);
99
+
100
+ const LAFS_SCHEMA_URL = 'https://lafs.dev/schemas/v1/envelope.schema.json';
101
+ const LAFS_TRANSPORTS: ReadonlySet<LAFSTransport> = new Set(['cli', 'http', 'grpc', 'sdk']);
102
+ const LAFS_MVI_LEVELS: ReadonlySet<MVILevel> = new Set(['minimal', 'standard', 'full', 'custom']);
103
+ const LAFS_ERROR_CATEGORIES: ReadonlySet<LAFSErrorCategory> = new Set([
104
+ 'VALIDATION',
105
+ 'AUTH',
106
+ 'PERMISSION',
107
+ 'NOT_FOUND',
108
+ 'CONFLICT',
109
+ 'RATE_LIMIT',
110
+ 'TRANSIENT',
111
+ 'INTERNAL',
112
+ 'CONTRACT',
113
+ 'MIGRATION',
114
+ ]);
115
+
116
+ function issue(path: string, message: string, keyword = 'type'): OperationEnvelopeValidationIssue {
117
+ return { path, code: `LAFS_SCHEMA_${keyword.toUpperCase()}`, keyword, message };
118
+ }
119
+
120
+ function isRecord(input: unknown): input is Record<string, unknown> {
121
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
122
+ }
123
+
124
+ function validateBaseLafsEnvelope(input: unknown): OperationEnvelopeValidationIssue[] {
125
+ if (!isRecord(input)) return [issue('/', 'LAFS envelope must be an object')];
126
+
127
+ const issues: OperationEnvelopeValidationIssue[] = [];
128
+ if (input.$schema !== LAFS_SCHEMA_URL) {
129
+ issues.push(issue('/$schema', `must equal ${LAFS_SCHEMA_URL}`, 'const'));
130
+ }
131
+
132
+ if (!isRecord(input._meta)) {
133
+ issues.push(issue('/_meta', 'must be an object'));
134
+ } else {
135
+ const meta = input._meta;
136
+ for (const field of ['specVersion', 'schemaVersion', 'timestamp', 'operation', 'requestId']) {
137
+ if (typeof meta[field] !== 'string' || meta[field].length === 0) {
138
+ issues.push(issue(`/_meta/${field}`, 'must be a non-empty string'));
139
+ }
140
+ }
141
+ if (
142
+ typeof meta.transport !== 'string' ||
143
+ !LAFS_TRANSPORTS.has(meta.transport as LAFSTransport)
144
+ ) {
145
+ issues.push(issue('/_meta/transport', 'must be a valid LAFS transport', 'enum'));
146
+ }
147
+ if (typeof meta.strict !== 'boolean') {
148
+ issues.push(issue('/_meta/strict', 'must be a boolean'));
149
+ }
150
+ if (typeof meta.mvi !== 'string' || !LAFS_MVI_LEVELS.has(meta.mvi as MVILevel)) {
151
+ issues.push(issue('/_meta/mvi', 'must be a valid LAFS MVI level', 'enum'));
152
+ }
153
+ if (!Number.isInteger(meta.contextVersion) || Number(meta.contextVersion) < 0) {
154
+ issues.push(issue('/_meta/contextVersion', 'must be a non-negative integer'));
155
+ }
156
+ }
157
+
158
+ if (typeof input.success !== 'boolean') {
159
+ issues.push(issue('/success', 'must be a boolean'));
160
+ }
161
+ if (!('result' in input)) {
162
+ issues.push(issue('/result', 'is required', 'required'));
163
+ } else if (input.result !== null && !isRecord(input.result) && !Array.isArray(input.result)) {
164
+ issues.push(issue('/result', 'must be an object, array, or null'));
165
+ }
166
+
167
+ if (input.error !== null && input.error !== undefined) {
168
+ if (!isRecord(input.error)) {
169
+ issues.push(issue('/error', 'must be an object or null'));
170
+ } else {
171
+ const error = input.error;
172
+ if (typeof error.code !== 'string' || error.code.length === 0) {
173
+ issues.push(issue('/error/code', 'must be a non-empty string'));
174
+ }
175
+ if (typeof error.message !== 'string' || error.message.length === 0) {
176
+ issues.push(issue('/error/message', 'must be a non-empty string'));
177
+ }
178
+ if (
179
+ typeof error.category !== 'string' ||
180
+ !LAFS_ERROR_CATEGORIES.has(error.category as LAFSErrorCategory)
181
+ ) {
182
+ issues.push(issue('/error/category', 'must be a valid LAFS error category', 'enum'));
183
+ }
184
+ if (typeof error.retryable !== 'boolean') {
185
+ issues.push(issue('/error/retryable', 'must be a boolean'));
186
+ }
187
+ if (
188
+ error.retryAfterMs !== null &&
189
+ error.retryAfterMs !== undefined &&
190
+ (!Number.isInteger(error.retryAfterMs) || Number(error.retryAfterMs) < 0)
191
+ ) {
192
+ issues.push(issue('/error/retryAfterMs', 'must be a non-negative integer or null'));
193
+ }
194
+ if (!isRecord(error.details)) {
195
+ issues.push(issue('/error/details', 'must be an object'));
196
+ }
197
+ }
198
+ }
199
+
200
+ return issues;
201
+ }
202
+
203
+ function pointer(path: readonly PropertyKey[]): string {
204
+ if (path.length === 0) return '/result';
205
+ return `/result/${path
206
+ .map((segment) => String(segment).split('~').join('~0').split('/').join('~1'))
207
+ .join('/')}`;
208
+ }
209
+
210
+ function resolveOperation(
211
+ operationName: string,
212
+ operations: readonly OperationDef[],
213
+ ): OperationDef | undefined {
214
+ return operations.find(
215
+ (candidate) => `${candidate.domain}.${candidate.operation}` === operationName,
216
+ );
217
+ }
218
+
219
+ function hasCanonicalSuccessShape(envelope: LAFSEnvelope): OperationEnvelopeValidationIssue[] {
220
+ if (envelope.success) {
221
+ if (envelope.result === null) {
222
+ return [
223
+ {
224
+ path: '/result',
225
+ code: E_LAFS_OPERATION_ERROR_SHAPE,
226
+ message: 'successful LAFS envelopes must include a non-null result payload',
227
+ },
228
+ ];
229
+ }
230
+ if (envelope.error !== null && envelope.error !== undefined) {
231
+ return [
232
+ {
233
+ path: '/error',
234
+ code: E_LAFS_OPERATION_ERROR_SHAPE,
235
+ message: 'successful LAFS envelopes must not include an error payload',
236
+ },
237
+ ];
238
+ }
239
+ return [];
240
+ }
241
+
242
+ const issues: OperationEnvelopeValidationIssue[] = [];
243
+ if (envelope.error === null || envelope.error === undefined) {
244
+ issues.push({
245
+ path: '/error',
246
+ code: E_LAFS_OPERATION_ERROR_SHAPE,
247
+ message: 'failed LAFS envelopes must include the canonical error payload',
248
+ });
249
+ }
250
+ if (envelope.result !== null) {
251
+ issues.push({
252
+ path: '/result',
253
+ code: E_LAFS_OPERATION_ERROR_SHAPE,
254
+ message: 'failed LAFS envelopes must set result to null',
255
+ });
256
+ }
257
+ return issues;
258
+ }
259
+
260
+ /**
261
+ * Validate a LAFS envelope against the dispatch operation registry and known
262
+ * operation result contracts.
263
+ */
264
+ export function validateOperationEnvelope(
265
+ input: unknown,
266
+ options: OperationEnvelopeValidationOptions = {},
267
+ ): OperationEnvelopeValidationResult {
268
+ const issues: OperationEnvelopeValidationIssue[] = validateBaseLafsEnvelope(input);
269
+
270
+ if (issues.length > 0) {
271
+ return {
272
+ valid: false,
273
+ envelopeValid: false,
274
+ issues,
275
+ errors: issues.map((issue) => `${issue.path} ${issue.message}`.trim()),
276
+ };
277
+ }
278
+
279
+ const envelope = input as LAFSEnvelope;
280
+ const operationName = envelope._meta.operation;
281
+ const operations = options.operations ?? OPERATIONS;
282
+ const resultSchemas = options.resultSchemas ?? OPERATION_RESULT_SCHEMAS;
283
+ const operation = resolveOperation(operationName, operations);
284
+
285
+ if (operation === undefined) {
286
+ issues.push({
287
+ path: '/_meta/operation',
288
+ code: E_LAFS_OPERATION_UNREGISTERED,
289
+ message: `unknown LAFS operation '${operationName}'`,
290
+ });
291
+ }
292
+
293
+ issues.push(...hasCanonicalSuccessShape(envelope));
294
+
295
+ const schema = resultSchemas.get(operationName as OperationEnvelopeName);
296
+ if (envelope.success && schema !== undefined && envelope.result !== null) {
297
+ const parsed = schema.safeParse(envelope.result);
298
+ if (!parsed.success) {
299
+ for (const zodIssue of parsed.error.issues) {
300
+ issues.push({
301
+ path: pointer(zodIssue.path),
302
+ code: E_LAFS_OPERATION_RESULT_SCHEMA,
303
+ keyword: zodIssue.code,
304
+ message: zodIssue.message,
305
+ });
306
+ }
307
+ }
308
+ }
309
+
310
+ return {
311
+ valid: issues.length === 0,
312
+ envelopeValid: true,
313
+ operation,
314
+ resultSchemaOperation:
315
+ schema === undefined ? undefined : (operationName as OperationEnvelopeName),
316
+ issues,
317
+ errors: issues.map((issue) => `${issue.path} ${issue.message}`.trim()),
318
+ };
319
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tests for T10555 — agent operation contract doctrine.
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import {
7
+ AGENT_OPERATION_CONTRACT_DOCTRINE,
8
+ type AgentMutationResult,
9
+ type AgentOperationDryRun,
10
+ type AgentOperationEmptyState,
11
+ type AgentOperationSessionLineage,
12
+ } from '../agent-operation-contract.js';
13
+
14
+ interface SampleTaskMutation {
15
+ readonly title: string;
16
+ }
17
+
18
+ interface SampleMutationData {
19
+ readonly taskIds: string[];
20
+ }
21
+
22
+ const session: AgentOperationSessionLineage = {
23
+ executionSessionId: 'ses_worker_1',
24
+ originSessionId: 'ses_orchestrator_1',
25
+ };
26
+
27
+ describe('AgentMutationResult doctrine', () => {
28
+ it('standardizes mutation result shape with status, ok, data, items, summary, issues, and session', () => {
29
+ const result: AgentMutationResult<SampleMutationData, SampleTaskMutation> = {
30
+ status: 'success',
31
+ ok: true,
32
+ data: { taskIds: ['T1'] },
33
+ items: [
34
+ {
35
+ id: 'T1',
36
+ effect: 'applied',
37
+ item: { title: 'ship contract' },
38
+ issues: [],
39
+ },
40
+ ],
41
+ summary: {
42
+ requested: 1,
43
+ applied: 1,
44
+ planned: 0,
45
+ skipped: 0,
46
+ failed: 0,
47
+ },
48
+ issues: [],
49
+ session,
50
+ };
51
+
52
+ expect(result).toMatchObject({
53
+ status: 'success',
54
+ ok: true,
55
+ data: { taskIds: ['T1'] },
56
+ summary: { requested: 1, applied: 1, planned: 0, skipped: 0, failed: 0 },
57
+ session,
58
+ });
59
+ expect(AGENT_OPERATION_CONTRACT_DOCTRINE.mutationStatuses).toEqual([
60
+ 'success',
61
+ 'partial_success',
62
+ 'empty',
63
+ 'dry_run',
64
+ 'failed',
65
+ ]);
66
+ });
67
+
68
+ it('defines partial-success as durable writes plus skipped or failed items', () => {
69
+ const result: AgentMutationResult<SampleMutationData, SampleTaskMutation> = {
70
+ status: 'partial_success',
71
+ ok: true,
72
+ data: { taskIds: ['T1'] },
73
+ items: [
74
+ { id: 'T1', effect: 'applied', item: { title: 'applied' }, issues: [] },
75
+ {
76
+ id: 'T2',
77
+ effect: 'failed',
78
+ item: { title: 'rejected' },
79
+ issues: [
80
+ { code: 'E_TASK_LOCKED', severity: 'error', message: 'Task is locked', target: 'T2' },
81
+ ],
82
+ },
83
+ ],
84
+ summary: {
85
+ requested: 2,
86
+ applied: 1,
87
+ planned: 0,
88
+ skipped: 0,
89
+ failed: 1,
90
+ },
91
+ issues: [
92
+ { code: 'E_TASK_LOCKED', severity: 'error', message: 'Task is locked', target: 'T2' },
93
+ ],
94
+ session,
95
+ };
96
+
97
+ expect(result.status).toBe(AGENT_OPERATION_CONTRACT_DOCTRINE.partialSuccess.status);
98
+ expect(result.summary.applied).toBeGreaterThanOrEqual(
99
+ AGENT_OPERATION_CONTRACT_DOCTRINE.partialSuccess.requiresAppliedMinimum,
100
+ );
101
+ expect(result.summary.failed + result.summary.skipped).toBeGreaterThanOrEqual(
102
+ AGENT_OPERATION_CONTRACT_DOCTRINE.partialSuccess.requiresRejectedOrSkippedMinimum,
103
+ );
104
+ expect(AGENT_OPERATION_CONTRACT_DOCTRINE.partialSuccess.inspect).toEqual(['items', 'issues']);
105
+ });
106
+
107
+ it('distinguishes executionSessionId from originSessionId for delegated operation lineage', () => {
108
+ expect(session.executionSessionId).toBe('ses_worker_1');
109
+ expect(session.originSessionId).toBe('ses_orchestrator_1');
110
+ expect(AGENT_OPERATION_CONTRACT_DOCTRINE.sessionLineage.executionSessionId).toContain(
111
+ 'executed',
112
+ );
113
+ expect(AGENT_OPERATION_CONTRACT_DOCTRINE.sessionLineage.originSessionId).toContain(
114
+ 'originated',
115
+ );
116
+ });
117
+
118
+ it('defines typed empty-state and dry-run semantics', () => {
119
+ const emptyState: AgentOperationEmptyState = {
120
+ code: 'EMPTY_NO_CANDIDATES',
121
+ reason: 'No candidate tasks matched the mutation filter.',
122
+ nextAction: 'Create or select a task before retrying.',
123
+ };
124
+ const dryRun: AgentOperationDryRun = {
125
+ enabled: true,
126
+ plannedWrites: 2,
127
+ appliedWrites: 0,
128
+ };
129
+
130
+ const emptyResult: AgentMutationResult<SampleMutationData, SampleTaskMutation> = {
131
+ status: 'empty',
132
+ ok: true,
133
+ data: { taskIds: [] },
134
+ items: [],
135
+ summary: { requested: 0, applied: 0, planned: 0, skipped: 0, failed: 0 },
136
+ issues: [],
137
+ session,
138
+ emptyState,
139
+ };
140
+ const dryRunResult: AgentMutationResult<SampleMutationData, SampleTaskMutation> = {
141
+ status: 'dry_run',
142
+ ok: true,
143
+ data: { taskIds: ['T1', 'T2'] },
144
+ items: [
145
+ { id: 'T1', effect: 'planned', item: { title: 'preview one' }, issues: [] },
146
+ { id: 'T2', effect: 'planned', item: { title: 'preview two' }, issues: [] },
147
+ ],
148
+ summary: { requested: 2, applied: 0, planned: 2, skipped: 0, failed: 0 },
149
+ issues: [],
150
+ session,
151
+ dryRun,
152
+ };
153
+
154
+ expect(emptyResult.emptyState).toEqual(emptyState);
155
+ expect(dryRunResult.dryRun).toEqual(dryRun);
156
+ expect(dryRunResult.items.every((item) => item.effect === 'planned')).toBe(true);
157
+ expect(dryRunResult.summary.applied).toBe(
158
+ AGENT_OPERATION_CONTRACT_DOCTRINE.dryRun.appliedWrites,
159
+ );
160
+ });
161
+ });
@@ -73,6 +73,36 @@ describe('tasksAddBatchInputContract', () => {
73
73
  expect(tasksAddBatchInputContract.schema['additionalProperties']).toBe(false);
74
74
  });
75
75
 
76
+ it('keeps labels and acceptance canonical as arrays in the add-batch schema', () => {
77
+ const schema = tasksAddBatchInputContract.schema as {
78
+ properties: {
79
+ tasks: {
80
+ items: {
81
+ properties: {
82
+ labels: { type: string; items: { type: string }; 'x-fix-hint'?: string };
83
+ acceptance: { type: string; items: { type: string }; 'x-fix-hint'?: string };
84
+ };
85
+ };
86
+ };
87
+ };
88
+ };
89
+ const entryProperties = schema.properties.tasks.items.properties;
90
+
91
+ expect(entryProperties.labels).toMatchObject({ type: 'array', items: { type: 'string' } });
92
+ expect(entryProperties.labels['x-fix-hint']).toContain('JSON array');
93
+ expect(entryProperties.acceptance).toMatchObject({ type: 'array', items: { type: 'string' } });
94
+ expect(entryProperties.acceptance['x-fix-hint']).toContain('not a pipe-delimited string');
95
+ });
96
+
97
+ it('ships add-batch examples with canonical labels and acceptance arrays', () => {
98
+ const withArrays = tasksAddBatchInputContract.examples.find((ex) => ex.name === 'two-tasks');
99
+ expect(withArrays).toBeDefined();
100
+ const firstTask = withArrays?.value.tasks[0];
101
+
102
+ expect(Array.isArray(firstTask?.labels)).toBe(true);
103
+ expect(Array.isArray(firstTask?.acceptance)).toBe(true);
104
+ });
105
+
76
106
  it('exports the raw schema constant for re-use', () => {
77
107
  expect(TASKS_ADD_BATCH_INPUT_SCHEMA).toBe(tasksAddBatchInputContract.schema);
78
108
  });