@cleocode/contracts 2026.6.5 → 2026.6.7

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 (38) hide show
  1. package/dist/__tests__/operation-def.test.js.map +1 -1
  2. package/dist/dispatch/operation-def.d.ts +42 -0
  3. package/dist/dispatch/operation-def.d.ts.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/llm/provider-profile.d.ts +54 -0
  9. package/dist/llm/provider-profile.d.ts.map +1 -1
  10. package/dist/llm/resolved-descriptor.d.ts +111 -0
  11. package/dist/llm/resolved-descriptor.d.ts.map +1 -0
  12. package/dist/llm/resolved-descriptor.js +30 -0
  13. package/dist/llm/resolved-descriptor.js.map +1 -0
  14. package/dist/operations/__tests__/output-contracts.test.d.ts +16 -0
  15. package/dist/operations/__tests__/output-contracts.test.d.ts.map +1 -0
  16. package/dist/operations/__tests__/output-contracts.test.js +103 -0
  17. package/dist/operations/__tests__/output-contracts.test.js.map +1 -0
  18. package/dist/operations/output-contract.d.ts +127 -0
  19. package/dist/operations/output-contract.d.ts.map +1 -0
  20. package/dist/operations/output-contract.js +41 -0
  21. package/dist/operations/output-contract.js.map +1 -0
  22. package/dist/operations/output-contracts-data.d.ts +35 -0
  23. package/dist/operations/output-contracts-data.d.ts.map +1 -0
  24. package/dist/operations/output-contracts-data.js +328 -0
  25. package/dist/operations/output-contracts-data.js.map +1 -0
  26. package/dist/operations/tasks.d.ts +14 -4
  27. package/dist/operations/tasks.d.ts.map +1 -1
  28. package/dist/operations/tasks.js.map +1 -1
  29. package/package.json +2 -2
  30. package/src/__tests__/operation-def.test.ts +5 -0
  31. package/src/dispatch/operation-def.ts +44 -0
  32. package/src/index.ts +11 -0
  33. package/src/llm/provider-profile.ts +59 -0
  34. package/src/llm/resolved-descriptor.ts +113 -0
  35. package/src/operations/__tests__/output-contracts.test.ts +125 -0
  36. package/src/operations/output-contract.ts +140 -0
  37. package/src/operations/output-contracts-data.ts +356 -0
  38. package/src/operations/tasks.ts +14 -4
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Tests for the per-operation OUTPUT contract SSoT (T11692 / DHQ-057).
3
+ *
4
+ * Pins the proof-set of OUTPUT contracts and — critically — encodes the exact
5
+ * shape fact that produced the bug: `tasks.show` returns the task under `task`,
6
+ * so the canonical pointer is `/data/task/title`, NOT `/data/title`.
7
+ *
8
+ * Also validates that mutation contracts reflect the MinimalMutateEnvelope
9
+ * shape (string[] ids, NOT object arrays) and that tasks.find uses the
10
+ * { results, total } wrapper (NOT a bare array).
11
+ *
12
+ * @task T11692
13
+ * @epic T11679
14
+ */
15
+
16
+ import { describe, expect, it } from 'vitest';
17
+ import type { OperationOutputContract } from '../output-contract.js';
18
+ import { OUTPUT_CONTRACTS } from '../output-contracts-data.js';
19
+
20
+ describe('operations/output-contracts (T11692 · DHQ-057)', () => {
21
+ it('registers the high-traffic proof-set operations', () => {
22
+ for (const op of [
23
+ 'tasks.show',
24
+ 'tasks.list',
25
+ 'tasks.find',
26
+ 'tasks.add',
27
+ 'tasks.add-batch',
28
+ 'tasks.update',
29
+ 'tasks.complete',
30
+ ]) {
31
+ expect(OUTPUT_CONTRACTS[op], `missing output contract for ${op}`).toBeDefined();
32
+ }
33
+ });
34
+
35
+ it('keys every entry by its own operation id', () => {
36
+ for (const [key, contract] of Object.entries(OUTPUT_CONTRACTS)) {
37
+ expect(contract.operation).toBe(key);
38
+ }
39
+ });
40
+
41
+ describe('tasks.show — the operation that bit us', () => {
42
+ const show = OUTPUT_CONTRACTS['tasks.show'] as OperationOutputContract;
43
+
44
+ it('encodes that the task is nested under `task` (data schema)', () => {
45
+ const schema = show.dataSchema as {
46
+ required?: string[];
47
+ properties?: Record<string, { properties?: Record<string, unknown> }>;
48
+ };
49
+ expect(schema.required).toContain('task');
50
+ // The title lives under task.properties.title — NOT at the data root.
51
+ expect(schema.properties?.task?.properties).toHaveProperty('title');
52
+ expect(schema.properties).not.toHaveProperty('title');
53
+ });
54
+
55
+ it('lists /data/task/title as a valid --field pointer and NOT /data/title', () => {
56
+ expect(show.fieldPointers).toContain('/data/task/title');
57
+ expect(show.fieldPointers).not.toContain('/data/title');
58
+ });
59
+
60
+ it('carries a shapeNote steering agents away from /data/<field>', () => {
61
+ expect(show.shapeNote).toBeTruthy();
62
+ expect(show.shapeNote).toContain('/data/task/');
63
+ });
64
+
65
+ it('notes that view may be null', () => {
66
+ expect(show.shapeNote).toContain('null');
67
+ });
68
+ });
69
+
70
+ describe('mutation envelopes — MinimalMutateEnvelope (string[] ids, T9931)', () => {
71
+ it('tasks.add surfaces the created task ID as a bare string at /data/created/0', () => {
72
+ const add = OUTPUT_CONTRACTS['tasks.add'] as OperationOutputContract;
73
+ // Correct: /data/created/0 → "T11692" (string), NOT /data/created/0/id
74
+ expect(add.fieldPointers).toContain('/data/created/0');
75
+ expect(add.fieldPointers).not.toContain('/data/created/0/id');
76
+ // Schema must reflect string[] items, not object[]
77
+ const schema = add.dataSchema as {
78
+ required?: string[];
79
+ properties?: Record<string, { items?: { type?: string } }>;
80
+ };
81
+ expect(schema.required).toEqual(
82
+ expect.arrayContaining(['count', 'created', 'updated', 'deleted']),
83
+ );
84
+ expect(schema.properties?.created?.items?.type).toBe('string');
85
+ expect(schema.properties?.updated?.items?.type).toBe('string');
86
+ expect(schema.properties?.deleted?.items?.type).toBe('string');
87
+ });
88
+
89
+ it('tasks.update / tasks.complete surface the changed task ID at /data/updated/0 (string)', () => {
90
+ for (const op of ['tasks.update', 'tasks.complete']) {
91
+ const c = OUTPUT_CONTRACTS[op] as OperationOutputContract;
92
+ // Correct: /data/updated/0 → "T11692" (string), NOT /data/updated/0/id
93
+ expect(c.fieldPointers).toContain('/data/updated/0');
94
+ expect(c.fieldPointers).not.toContain('/data/updated/0/id');
95
+ const schema = c.dataSchema as {
96
+ properties?: Record<string, { items?: { type?: string } }>;
97
+ };
98
+ expect(schema.properties?.updated?.items?.type).toBe('string');
99
+ }
100
+ });
101
+
102
+ it('tasks.add-batch dry-run pointers point to root /data/wouldCreate and /data/insertedCount', () => {
103
+ const batch = OUTPUT_CONTRACTS['tasks.add-batch'] as OperationOutputContract;
104
+ // Correct: root-level, NOT /data/dryRunSummary/wouldCreate
105
+ expect(batch.fieldPointers).toContain('/data/wouldCreate');
106
+ expect(batch.fieldPointers).toContain('/data/insertedCount');
107
+ expect(batch.fieldPointers).not.toContain('/data/dryRunSummary/wouldCreate');
108
+ expect(batch.fieldPointers).not.toContain('/data/dryRunSummary/insertedCount');
109
+ // shapeNote should clarify they are NOT under dryRunSummary
110
+ expect(batch.shapeNote).toContain('/data/wouldCreate');
111
+ });
112
+ });
113
+
114
+ it('tasks.find data is a wrapper object with /data/results (array) and /data/total', () => {
115
+ const find = OUTPUT_CONTRACTS['tasks.find'] as OperationOutputContract;
116
+ // Correct: wrapper object, NOT a bare array
117
+ expect((find.dataSchema as { type?: string }).type).toBe('object');
118
+ // Correct pointers
119
+ expect(find.fieldPointers).toContain('/data/results/0/id');
120
+ expect(find.fieldPointers).toContain('/data/total');
121
+ // Wrong pointers must NOT appear
122
+ expect(find.fieldPointers).not.toContain('/data/0/id');
123
+ expect(find.shapeNote).toContain('/data/results');
124
+ });
125
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * OperationOutputContract — schema-first OUTPUT contract for CLEO operations.
3
+ *
4
+ * This module is the SINGLE SOURCE OF TRUTH for the per-operation OUTPUT
5
+ * (result) shape introspection surface (DHQ-057 · T11692 · EP-DHQ-CORE-FIXES
6
+ * T11679 · SG-CORE-SELF-TOOLING T11480). It is the OUTPUT-side mirror of
7
+ * {@link OperationInputContract}.
8
+ *
9
+ * ## Why this exists
10
+ *
11
+ * Before this contract, {@link OperationDef} declared INPUT (`params`) but had
12
+ * NO machine-readable description of the OUTPUT envelope's `data` payload.
13
+ * Consequently:
14
+ *
15
+ * - `cleo show T123 --field /data/title` returned `E_FIELD_NOT_FOUND` because
16
+ * the real shape is `/data/task/title` — agents had no way to predict the
17
+ * envelope `data` shape and `--field` JSON pointers were guesswork.
18
+ * - Every consumer (CLI `--field`, REST clients, the SDK) hand-sniffed the
19
+ * result per verb instead of resolving against a declared contract.
20
+ *
21
+ * An {@link OperationOutputContract} pairs a stable operation identifier with
22
+ * the JSON Schema (draft-07) document describing the envelope's `data` payload
23
+ * AND the curated set of valid `--field` JSON pointers (rooted at `/data`) that
24
+ * an agent can use without trial and error.
25
+ *
26
+ * ZERO runtime dependencies: `contracts` is a leaf package. JSON Schema
27
+ * documents are modeled as the loose {@link JsonSchema} alias (re-used from the
28
+ * input-contract module) so this module does not pull in a JSON Schema typings
29
+ * package.
30
+ *
31
+ * @packageDocumentation
32
+ * @module @cleocode/contracts/operations/output-contract
33
+ *
34
+ * @see {@link https://json-schema.org/draft-07/schema}
35
+ * @see OperationInputContract — the INPUT-side mirror of this contract
36
+ *
37
+ * @epic T11679
38
+ * @task T11692 — DHQ-057: per-operation output schema SSoT
39
+ */
40
+
41
+ import type { JsonSchema } from './input-contract.js';
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // OperationOutputContract
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Schema-first OUTPUT (result) contract for a single CLEO operation.
49
+ *
50
+ * Pairs a stable operation identifier with the JSON Schema draft-07 document
51
+ * that describes the LAFS envelope's `data` payload, plus the curated list of
52
+ * valid `--field` JSON pointers an agent can rely on.
53
+ *
54
+ * The `dataSchema` describes the shape of `envelope.data` — i.e. the value the
55
+ * dispatch handler returns, BEFORE the LAFS `{ success, data, meta }` wrapper
56
+ * is applied. For example, `tasks.show` returns
57
+ * `{ task, view, attachments, acRows?, relations? }`, so its `dataSchema` has
58
+ * `task` as a required property and `/data/task/title` is the canonical pointer
59
+ * for the task title — NOT `/data/title`.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const showOutput: OperationOutputContract = {
64
+ * operation: 'tasks.show',
65
+ * dataSchema: {
66
+ * type: 'object',
67
+ * required: ['task'],
68
+ * properties: {
69
+ * task: { type: 'object', description: 'Full task record' },
70
+ * view: { type: ['object', 'null'] },
71
+ * attachments: { type: 'array' },
72
+ * },
73
+ * },
74
+ * fieldPointers: ['/data/task/id', '/data/task/title', '/data/task/status'],
75
+ * };
76
+ * ```
77
+ */
78
+ export interface OperationOutputContract {
79
+ /**
80
+ * Stable, fully-qualified operation identifier in `<domain>.<verb>` form
81
+ * (e.g. `'tasks.show'`, `'tasks.add-batch'`). Used as the registry key in
82
+ * {@link OperationOutputContractRegistry}.
83
+ */
84
+ operation: string;
85
+
86
+ /**
87
+ * JSON Schema draft-07 document describing the shape of the LAFS envelope's
88
+ * `data` payload returned by this operation.
89
+ *
90
+ * This is the shape of `envelope.data` — the dispatch handler's return value
91
+ * BEFORE the `{ success, data, meta }` wrapper. Pointers used by `--field`
92
+ * are therefore rooted at `/data` (e.g. `/data/task/title` resolves against
93
+ * `dataSchema.properties.task.properties.title`).
94
+ */
95
+ dataSchema: JsonSchema;
96
+
97
+ /**
98
+ * Curated, copy-pasteable list of valid `--field` JSON pointers an agent can
99
+ * use without trial and error. Each pointer is rooted at the envelope root
100
+ * (`/data/...`), matching the resolution semantics of `cleo <op> --field`.
101
+ *
102
+ * This is the direct remediation for the `--field /data/title` →
103
+ * `E_FIELD_NOT_FOUND` class of failures: instead of guessing, an agent runs
104
+ * `cleo show --describe` and reads the exact pointers that resolve.
105
+ *
106
+ * Conventionally lists the high-value scalar/leaf pointers (ids, titles,
107
+ * statuses, counts) — not every possible deep pointer.
108
+ */
109
+ fieldPointers: string[];
110
+
111
+ /**
112
+ * Optional one-line note clarifying non-obvious shape facts (e.g. "task body
113
+ * is nested under `task`, not the root"). Surfaced in `--describe` output to
114
+ * pre-empt the exact mistakes this contract is designed to prevent.
115
+ */
116
+ shapeNote?: string;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Registry
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Registry of every {@link OperationOutputContract} known to the CLEO runtime,
125
+ * keyed by the contract's `operation` identifier (e.g. `'tasks.show'`).
126
+ *
127
+ * The OUTPUT-side mirror of `OperationInputContractRegistry`. Populated
128
+ * incrementally — high-traffic operations first. A `null` lookup (operation
129
+ * absent from the registry) is expected and MUST NOT error: callers render a
130
+ * structured "no output contract yet" payload instead.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const registry: OperationOutputContractRegistry = {
135
+ * 'tasks.show': showOutputContract,
136
+ * 'tasks.list': listOutputContract,
137
+ * };
138
+ * ```
139
+ */
140
+ export type OperationOutputContractRegistry = Record<string, OperationOutputContract>;
@@ -0,0 +1,356 @@
1
+ /**
2
+ * OUTPUT_CONTRACTS data — the proof-set of per-operation OUTPUT contracts.
3
+ *
4
+ * SSoT for the OUTPUT (result) shape of CLEO's highest-traffic operations
5
+ * (DHQ-057 · T11692). Each entry is an {@link OperationOutputContract}: a
6
+ * JSON Schema draft-07 document for the LAFS envelope's `data` payload plus
7
+ * the curated list of valid `--field` JSON pointers.
8
+ *
9
+ * Populated INCREMENTALLY — high-traffic operations first. This is the OUTPUT
10
+ * mirror of `INPUT_CONTRACTS` (which lives in core). The data lives here in
11
+ * `contracts` (a leaf package, zero runtime deps) so every consumer — CLI
12
+ * `--describe`, the SDK `describeOperation`, REST clients — resolves against
13
+ * one source of truth.
14
+ *
15
+ * Each `dataSchema` is grounded in the concrete result interface declared in
16
+ * `./tasks.js` (e.g. {@link TasksShowResult}, {@link MinimalMutateEnvelope}) so
17
+ * the schema can never drift from the typed shape without a compile-time touch
18
+ * to this file.
19
+ *
20
+ * @packageDocumentation
21
+ * @module @cleocode/contracts/operations/output-contracts-data
22
+ *
23
+ * @epic T11679
24
+ * @task T11692 — DHQ-057: per-operation output schema SSoT
25
+ */
26
+
27
+ import type {
28
+ OperationOutputContract,
29
+ OperationOutputContractRegistry,
30
+ } from './output-contract.js';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // tasks.show — the operation that bit us (DHQ-057 reproduction)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * OUTPUT contract for `tasks.show`.
38
+ *
39
+ * Grounded in {@link TasksShowResult}: `{ task, view, attachments, acRows?,
40
+ * relations? }`. The task body is nested under `task` — so the canonical
41
+ * pointer for the title is `/data/task/title`, NOT `/data/title`. This is the
42
+ * exact shape whose absence produced `cleo show --field /data/title` →
43
+ * `E_FIELD_NOT_FOUND`.
44
+ */
45
+ const tasksShowOutputContract: OperationOutputContract = {
46
+ operation: 'tasks.show',
47
+ shapeNote:
48
+ 'The task record is nested under `task` — use /data/task/<field>, not /data/<field>. ' +
49
+ '`view` may be null. `acRows` and `relations` are conditional.',
50
+ dataSchema: {
51
+ type: 'object',
52
+ required: ['task', 'view', 'attachments'],
53
+ additionalProperties: true,
54
+ properties: {
55
+ task: {
56
+ type: 'object',
57
+ description: 'Full task record (id, title, status, priority, type, parentId, ...).',
58
+ required: ['id', 'title', 'status'],
59
+ properties: {
60
+ id: { type: 'string' },
61
+ title: { type: 'string' },
62
+ status: { type: 'string' },
63
+ priority: { type: 'string' },
64
+ type: { type: 'string' },
65
+ parentId: { type: ['string', 'null'] },
66
+ },
67
+ },
68
+ view: {
69
+ type: ['object', 'null'],
70
+ description:
71
+ 'Canonical task view projection produced by computeTaskView. Null when unavailable.',
72
+ },
73
+ attachments: {
74
+ type: 'array',
75
+ description: 'Docs attachments linked to this task. Always an array (empty when none).',
76
+ },
77
+ acRows: {
78
+ type: 'array',
79
+ description: 'Acceptance-criterion rows (id, alias AC<n>, ordinal, text). Optional.',
80
+ },
81
+ relations: {
82
+ type: 'object',
83
+ description: 'Expanded relation/doc lists. Present only with --relations.',
84
+ },
85
+ },
86
+ },
87
+ fieldPointers: [
88
+ '/data/task/id',
89
+ '/data/task/title',
90
+ '/data/task/status',
91
+ '/data/task/priority',
92
+ '/data/task/type',
93
+ '/data/task/parentId',
94
+ '/data/view/id',
95
+ '/data/view/title',
96
+ ],
97
+ };
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // tasks.list — array-of-tasks plus counts
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * OUTPUT contract for `tasks.list`.
105
+ *
106
+ * Grounded in {@link TasksListResult}: `{ tasks: TaskOp[], total, filtered }`.
107
+ */
108
+ const tasksListOutputContract: OperationOutputContract = {
109
+ operation: 'tasks.list',
110
+ shapeNote: 'Rows are under /data/tasks (an array); counts are /data/total and /data/filtered.',
111
+ dataSchema: {
112
+ type: 'object',
113
+ required: ['tasks', 'total', 'filtered'],
114
+ additionalProperties: true,
115
+ properties: {
116
+ tasks: {
117
+ type: 'array',
118
+ description: 'Task rows matching the filters.',
119
+ items: {
120
+ type: 'object',
121
+ properties: {
122
+ id: { type: 'string' },
123
+ title: { type: 'string' },
124
+ status: { type: 'string' },
125
+ },
126
+ },
127
+ },
128
+ total: { type: 'number', description: 'Total tasks before filtering.' },
129
+ filtered: { type: 'number', description: 'Number of tasks after filters applied.' },
130
+ },
131
+ },
132
+ fieldPointers: [
133
+ '/data/tasks/0/id',
134
+ '/data/tasks/0/title',
135
+ '/data/tasks/0/status',
136
+ '/data/total',
137
+ '/data/filtered',
138
+ ],
139
+ };
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // tasks.find — { results: MinimalTask[], total, query, searchType }
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * OUTPUT contract for `tasks.find`.
147
+ *
148
+ * Grounded in the actual return shape from `findTasks()` (find.ts:503-505)
149
+ * and the MVI projection plan (mvi-projection.ts:330 `path: 'results'`):
150
+ * `{ results: MinimalTask[], total: number, query: string, searchType: string }`.
151
+ *
152
+ * The `data` payload is a **wrapper object** (NOT a bare array). Results live
153
+ * under `/data/results`; the total count is at `/data/total`.
154
+ */
155
+ const tasksFindOutputContract: OperationOutputContract = {
156
+ operation: 'tasks.find',
157
+ shapeNote:
158
+ 'Results are wrapped: /data/results (array of matches), /data/total (count). ' +
159
+ 'Use /data/results/0/id — NOT /data/0/id.',
160
+ dataSchema: {
161
+ type: 'object',
162
+ required: ['results', 'total'],
163
+ additionalProperties: true,
164
+ properties: {
165
+ results: {
166
+ type: 'array',
167
+ description: 'Array of minimal task matches.',
168
+ items: {
169
+ type: 'object',
170
+ required: ['id', 'title'],
171
+ properties: {
172
+ id: { type: 'string' },
173
+ title: { type: 'string' },
174
+ status: { type: 'string' },
175
+ },
176
+ },
177
+ },
178
+ total: { type: 'number', description: 'Total matching tasks.' },
179
+ query: { type: 'string', description: 'The query string that was searched.' },
180
+ searchType: { type: 'string', description: 'Kind of search performed (fts, semantic, ...).' },
181
+ },
182
+ },
183
+ fieldPointers: [
184
+ '/data/results/0/id',
185
+ '/data/results/0/title',
186
+ '/data/results/0/status',
187
+ '/data/total',
188
+ ],
189
+ };
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Mutation envelope shape (T9931 / T10608) — shared by add / add-batch /
193
+ // update / complete / delete.
194
+ //
195
+ // DEFAULT (minimal) shape — what `--field` resolves against — is
196
+ // `MinimalMutateEnvelope` from `mutate-projection.ts`:
197
+ // { count, created: string[], updated: string[], deleted: string[], ids: string[], ... }
198
+ //
199
+ // `created`, `updated`, and `deleted` contain BARE TASK ID STRINGS, not
200
+ // objects. `/data/created/0` resolves to a string like "T11692" directly.
201
+ // The object-array shape (`TaskMutationEnvelope`) is the `--full` shape only.
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * The shared JSON Schema for the minimal mutate projection envelope
206
+ * (`MinimalMutateEnvelope` — T9931 / mutate-projection.ts). `created`,
207
+ * `updated`, and `deleted` are always present arrays of **bare task ID strings**
208
+ * (not objects). The gateway populates only the relevant bucket. Pointers:
209
+ * - `/data/created/0` → first created task ID string (e.g. "T11692")
210
+ * - `/data/updated/0` → first updated task ID string
211
+ * - `/data/deleted/0` → first deleted task ID string
212
+ *
213
+ * This is the DEFAULT shape (applies unless caller passes `--full`).
214
+ */
215
+ const TASK_MUTATION_DATA_SCHEMA = {
216
+ type: 'object',
217
+ required: ['count', 'created', 'updated', 'deleted'],
218
+ additionalProperties: true,
219
+ properties: {
220
+ count: { type: 'number', description: 'Number of records the mutation affected.' },
221
+ created: {
222
+ type: 'array',
223
+ description:
224
+ 'Task IDs created by the mutation (bare strings, e.g. "T11692"). ' +
225
+ 'Empty for update/delete-only mutations.',
226
+ items: { type: 'string' },
227
+ },
228
+ updated: {
229
+ type: 'array',
230
+ description:
231
+ 'Task IDs updated by the mutation (bare strings). ' +
232
+ 'Empty for create/delete-only mutations.',
233
+ items: { type: 'string' },
234
+ },
235
+ deleted: {
236
+ type: 'array',
237
+ description:
238
+ 'Task IDs deleted by the mutation (bare strings). ' +
239
+ 'Empty for create/update-only mutations.',
240
+ items: { type: 'string' },
241
+ },
242
+ ids: {
243
+ type: 'array',
244
+ description: 'Deprecated alias for the non-empty bucket. Prefer created/updated/deleted.',
245
+ items: { type: 'string' },
246
+ },
247
+ dryRun: { type: 'boolean', description: 'True when this was a preview-only mutation.' },
248
+ status: { type: 'string', description: 'Post-mutation task status (add/update/complete).' },
249
+ },
250
+ } as const;
251
+
252
+ /**
253
+ * OUTPUT contract for `tasks.add`.
254
+ *
255
+ * Grounded in `MinimalMutateEnvelope` (mutate-projection.ts) — the created
256
+ * task ID lands as a bare string in `created[0]`.
257
+ * Use `/data/created/0` (a string like "T11692"), NOT `/data/created/0/id`.
258
+ */
259
+ const tasksAddOutputContract: OperationOutputContract = {
260
+ operation: 'tasks.add',
261
+ shapeNote:
262
+ 'The created task ID (bare string) is at /data/created/0 — NOT /data/created/0/id. ' +
263
+ 'Example: /data/created/0 → "T11692".',
264
+ dataSchema: { ...TASK_MUTATION_DATA_SCHEMA },
265
+ fieldPointers: ['/data/created/0', '/data/count'],
266
+ };
267
+
268
+ /**
269
+ * OUTPUT contract for `tasks.add-batch`.
270
+ *
271
+ * Grounded in `MinimalMutateEnvelope` (mutate-projection.ts). For dry-run,
272
+ * `wouldCreate` and `insertedCount` are projected to the **root** of the
273
+ * envelope data (NOT under `dryRunSummary`) — confirmed in
274
+ * mutate-projection.ts lines 224/232.
275
+ */
276
+ const tasksAddBatchOutputContract: OperationOutputContract = {
277
+ operation: 'tasks.add-batch',
278
+ shapeNote:
279
+ 'Atomic batch insert. Each created task ID (bare string) is in /data/created (array). ' +
280
+ 'Dry-run projections are at root: /data/wouldCreate and /data/insertedCount (=0). ' +
281
+ 'NOT under /data/dryRunSummary.',
282
+ dataSchema: {
283
+ type: 'object',
284
+ required: ['count', 'created', 'updated', 'deleted'],
285
+ additionalProperties: true,
286
+ properties: {
287
+ ...TASK_MUTATION_DATA_SCHEMA.properties,
288
+ wouldCreate: {
289
+ type: 'number',
290
+ description: 'Dry-run: predicted write count. Present only when dryRun=true.',
291
+ },
292
+ insertedCount: {
293
+ type: 'number',
294
+ description: 'Dry-run: always 0 (no DB write). Present only when dryRun=true.',
295
+ },
296
+ wouldAffect: {
297
+ type: 'number',
298
+ description: 'Dry-run: generic affected count. Present only when dryRun=true.',
299
+ },
300
+ },
301
+ },
302
+ fieldPointers: ['/data/created/0', '/data/count', '/data/wouldCreate', '/data/insertedCount'],
303
+ };
304
+
305
+ /**
306
+ * OUTPUT contract for `tasks.update` (and the `complete` alias, which is an
307
+ * update to `status: done`).
308
+ *
309
+ * Grounded in `MinimalMutateEnvelope` (mutate-projection.ts) — the changed
310
+ * task ID lands as a bare string in `updated[0]`.
311
+ * Use `/data/updated/0` (a string like "T11692"), NOT `/data/updated/0/id`.
312
+ */
313
+ const tasksUpdateOutputContract: OperationOutputContract = {
314
+ operation: 'tasks.update',
315
+ shapeNote:
316
+ 'The updated task ID (bare string) is at /data/updated/0 — NOT /data/updated/0/id. ' +
317
+ 'Use /data/status for the post-mutation status.',
318
+ dataSchema: { ...TASK_MUTATION_DATA_SCHEMA },
319
+ fieldPointers: ['/data/updated/0', '/data/status', '/data/count'],
320
+ };
321
+
322
+ /**
323
+ * OUTPUT contract for `tasks.complete`.
324
+ *
325
+ * Identical envelope to `tasks.update` — completion is a status mutation that
326
+ * lands the task ID in `updated[0]` as a bare string.
327
+ */
328
+ const tasksCompleteOutputContract: OperationOutputContract = {
329
+ operation: 'tasks.complete',
330
+ shapeNote:
331
+ 'Completion is a status mutation — the task ID (bare string) is at /data/updated/0 ' +
332
+ '(status=done). Use /data/status for the post-mutation status.',
333
+ dataSchema: { ...TASK_MUTATION_DATA_SCHEMA },
334
+ fieldPointers: ['/data/updated/0', '/data/status', '/data/count'],
335
+ };
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Registry
339
+ // ---------------------------------------------------------------------------
340
+
341
+ /**
342
+ * SSoT registry of per-operation OUTPUT contracts, keyed by the canonical
343
+ * `<domain>.<verb>` operation id. Extend this map as operations are migrated
344
+ * to the schema-first OUTPUT surface (high-traffic first).
345
+ *
346
+ * @task T11692
347
+ */
348
+ export const OUTPUT_CONTRACTS: OperationOutputContractRegistry = {
349
+ 'tasks.show': tasksShowOutputContract,
350
+ 'tasks.list': tasksListOutputContract,
351
+ 'tasks.find': tasksFindOutputContract,
352
+ 'tasks.add': tasksAddOutputContract,
353
+ 'tasks.add-batch': tasksAddBatchOutputContract,
354
+ 'tasks.update': tasksUpdateOutputContract,
355
+ 'tasks.complete': tasksCompleteOutputContract,
356
+ };
@@ -959,8 +959,18 @@ export interface TasksSyncLinksRemoveResult {
959
959
  export interface TasksCancelParams {
960
960
  taskId: string;
961
961
  reason?: string;
962
- /** Explicit child handling mode. Defaults to 'block' so propagation is never implicit. */
963
- children?: 'block' | 'cascade' | 'orphan';
962
+ /**
963
+ * Explicit child handling mode. Defaults to 'block' so propagation is never
964
+ * implicit. `reparent` moves the children under {@link reparentTo}; the
965
+ * legacy `orphan`/detach mode was removed by T11811 (it manufactured the
966
+ * exact orphan the containment guard rejects).
967
+ */
968
+ children?: 'block' | 'cascade' | 'reparent';
969
+ /**
970
+ * Target parent ID for the `reparent` strategy (T11811). REQUIRED when
971
+ * `children='reparent'`.
972
+ */
973
+ reparentTo?: string;
964
974
  /** Operator waiver for large cascade cancellation. */
965
975
  force?: boolean;
966
976
  /** Large-subtree cascade guard threshold. Defaults to 10 descendants. */
@@ -992,8 +1002,8 @@ export interface TasksCancelResult {
992
1002
  */
993
1003
  alreadyCancelled?: boolean;
994
1004
  /** Explicit child handling mode applied by the operation. */
995
- childStrategy?: 'block' | 'cascade' | 'orphan';
996
- /** Child or descendant tasks affected by cascade/orphan handling. */
1005
+ childStrategy?: 'block' | 'cascade' | 'reparent';
1006
+ /** Child or descendant tasks affected by cascade/reparent handling. */
997
1007
  affectedTasks?: string[];
998
1008
  /** Count of affected child/descendant tasks. */
999
1009
  affectedCount?: number;