@cleocode/contracts 2026.5.122 → 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.
- package/dist/__tests__/completion-operation-contracts.test.d.ts +8 -0
- package/dist/__tests__/completion-operation-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/completion-operation-contracts.test.js +114 -0
- package/dist/__tests__/completion-operation-contracts.test.js.map +1 -0
- package/dist/__tests__/operation-envelope-validation.test.d.ts +8 -0
- package/dist/__tests__/operation-envelope-validation.test.d.ts.map +1 -0
- package/dist/__tests__/operation-envelope-validation.test.js +105 -0
- package/dist/__tests__/operation-envelope-validation.test.js.map +1 -0
- package/dist/__tests__/satisfies-atom.test.js +21 -3
- package/dist/__tests__/satisfies-atom.test.js.map +1 -1
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts +8 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.js +188 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.js.map +1 -0
- package/dist/__tests__/workgraph-operation-contracts.test.d.ts +8 -0
- package/dist/__tests__/workgraph-operation-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/workgraph-operation-contracts.test.js +115 -0
- package/dist/__tests__/workgraph-operation-contracts.test.js.map +1 -0
- package/dist/data-accessor.d.ts +77 -0
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/dispatch/__tests__/docs-update-registry.test.d.ts +2 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.d.ts.map +1 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.js +27 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.js.map +1 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts +2 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts.map +1 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js +265 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js.map +1 -0
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +487 -7
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +4 -4
- package/dist/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +9 -9
- package/dist/evidence-atom-schema.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/operation-envelope-validation.d.ts +79 -0
- package/dist/operation-envelope-validation.d.ts.map +1 -0
- package/dist/operation-envelope-validation.js +219 -0
- package/dist/operation-envelope-validation.js.map +1 -0
- package/dist/operations/__tests__/agent-operation-contract.test.d.ts +2 -0
- package/dist/operations/__tests__/agent-operation-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/agent-operation-contract.test.js +128 -0
- package/dist/operations/__tests__/agent-operation-contract.test.js.map +1 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js +15 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -1
- package/dist/operations/agent-operation-contract.d.ts +227 -0
- package/dist/operations/agent-operation-contract.d.ts.map +1 -0
- package/dist/operations/agent-operation-contract.js +38 -0
- package/dist/operations/agent-operation-contract.js.map +1 -0
- package/dist/operations/docs.d.ts +19 -1
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/docs.js +20 -0
- package/dist/operations/docs.js.map +1 -1
- package/dist/operations/focus.d.ts +1 -1
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +1 -0
- package/dist/operations/index.js.map +1 -1
- package/dist/operations/orchestrate.d.ts +66 -0
- package/dist/operations/orchestrate.d.ts.map +1 -1
- package/dist/operations/tasks.d.ts +568 -11
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +22 -3
- package/dist/operations/tasks.js.map +1 -1
- package/dist/task-record.d.ts +15 -0
- package/dist/task-record.d.ts.map +1 -1
- package/dist/task.d.ts +15 -6
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js.map +1 -1
- package/dist/tasks.d.ts +944 -0
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +198 -1
- package/dist/tasks.js.map +1 -1
- package/dist/workgraph.d.ts +1950 -0
- package/dist/workgraph.d.ts.map +1 -0
- package/dist/workgraph.js +449 -0
- package/dist/workgraph.js.map +1 -0
- package/package.json +10 -2
- package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
- package/src/__tests__/completion-operation-contracts.test.ts +154 -0
- package/src/__tests__/operation-envelope-validation.test.ts +140 -0
- package/src/__tests__/satisfies-atom.test.ts +23 -3
- package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
- package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
- package/src/data-accessor.ts +94 -1
- package/src/dispatch/__tests__/docs-update-registry.test.ts +35 -0
- package/src/dispatch/__tests__/operation-envelope-snapshots.test.ts +283 -0
- package/src/dispatch/operations-registry.ts +502 -7
- package/src/evidence-atom-schema.ts +10 -10
- package/src/index.ts +193 -2
- package/src/operation-envelope-validation.ts +319 -0
- package/src/operations/__tests__/agent-operation-contract.test.ts +161 -0
- package/src/operations/__tests__/tasks-input-contracts.test.ts +30 -0
- package/src/operations/agent-operation-contract.ts +253 -0
- package/src/operations/docs.ts +27 -7
- package/src/operations/focus.ts +1 -1
- package/src/operations/index.ts +1 -0
- package/src/operations/orchestrate.ts +71 -0
- package/src/operations/tasks.ts +612 -15
- package/src/task-record.ts +16 -0
- package/src/task.ts +16 -6
- package/src/tasks.ts +558 -0
- package/src/workgraph.ts +1153 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for the PM-Core V2 WorkGraph dispatch surface.
|
|
3
|
+
*
|
|
4
|
+
* @task T10606
|
|
5
|
+
* @saga T10538
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { OPERATIONS } from '../dispatch/operations-registry.js';
|
|
10
|
+
import {
|
|
11
|
+
tasksFrontierParamsSchema,
|
|
12
|
+
tasksFrontierResultSchema,
|
|
13
|
+
tasksRollupParamsSchema,
|
|
14
|
+
tasksRollupResultSchema,
|
|
15
|
+
tasksTraverseParamsSchema,
|
|
16
|
+
tasksTraverseResultSchema,
|
|
17
|
+
tasksTreeParamsSchema,
|
|
18
|
+
tasksTreeResultSchema,
|
|
19
|
+
tasksWorkGraphAuditParamsSchema,
|
|
20
|
+
tasksWorkGraphAuditResultSchema,
|
|
21
|
+
} from '../workgraph.js';
|
|
22
|
+
|
|
23
|
+
const requiredOperations = ['traverse', 'tree', 'rollup', 'frontier', 'workgraph.audit'] as const;
|
|
24
|
+
|
|
25
|
+
function operation(name: (typeof requiredOperations)[number]) {
|
|
26
|
+
const def = OPERATIONS.find(
|
|
27
|
+
(candidate) => candidate.domain === 'tasks' && candidate.operation === name,
|
|
28
|
+
);
|
|
29
|
+
if (def === undefined) throw new Error(`missing tasks.${name}`);
|
|
30
|
+
return def;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('WorkGraph operation contract schemas', () => {
|
|
34
|
+
it('validates params and results for every WorkGraph read operation', () => {
|
|
35
|
+
expect(
|
|
36
|
+
tasksTraverseParamsSchema.parse({
|
|
37
|
+
rootId: 'T1',
|
|
38
|
+
direction: 'descendants',
|
|
39
|
+
cursor: '0001:T2',
|
|
40
|
+
limit: 25,
|
|
41
|
+
}),
|
|
42
|
+
).toEqual({
|
|
43
|
+
rootId: 'T1',
|
|
44
|
+
direction: 'descendants',
|
|
45
|
+
cursor: '0001:T2',
|
|
46
|
+
limit: 25,
|
|
47
|
+
});
|
|
48
|
+
expect(
|
|
49
|
+
tasksTraverseResultSchema.parse({
|
|
50
|
+
rootId: 'T1',
|
|
51
|
+
direction: 'descendants',
|
|
52
|
+
nodes: [],
|
|
53
|
+
edges: [],
|
|
54
|
+
pageInfo: { hasMore: false },
|
|
55
|
+
}).pageInfo.hasMore,
|
|
56
|
+
).toBe(false);
|
|
57
|
+
|
|
58
|
+
expect(tasksTreeParamsSchema.parse({ rootId: 'T1', maxDepth: 2, limit: 10 })).toEqual({
|
|
59
|
+
rootId: 'T1',
|
|
60
|
+
maxDepth: 2,
|
|
61
|
+
limit: 10,
|
|
62
|
+
});
|
|
63
|
+
expect(
|
|
64
|
+
tasksTreeResultSchema.parse({
|
|
65
|
+
rootId: 'T1',
|
|
66
|
+
nodes: [],
|
|
67
|
+
edges: [],
|
|
68
|
+
pageInfo: { hasMore: false },
|
|
69
|
+
}).nodes,
|
|
70
|
+
).toEqual([]);
|
|
71
|
+
|
|
72
|
+
expect(tasksRollupParamsSchema.parse({ rootId: 'T1' })).toEqual({ rootId: 'T1' });
|
|
73
|
+
expect(
|
|
74
|
+
tasksRollupResultSchema.parse({
|
|
75
|
+
rootId: 'T1',
|
|
76
|
+
direct: { total: 0, byStatus: {}, byType: {} },
|
|
77
|
+
subtree: { total: 0, byStatus: {}, byType: {} },
|
|
78
|
+
percentDenominator: { basis: 'subtree-total', total: 0, description: 'subtree total' },
|
|
79
|
+
percentages: { done: 0, active: 0, blocked: 0, pending: 0, cancelled: 0 },
|
|
80
|
+
staleProjection: false,
|
|
81
|
+
projectionMismatches: [],
|
|
82
|
+
}).staleProjection,
|
|
83
|
+
).toBe(false);
|
|
84
|
+
|
|
85
|
+
expect(tasksFrontierParamsSchema.parse({ rootId: 'T1', role: 'worker' })).toEqual({
|
|
86
|
+
rootId: 'T1',
|
|
87
|
+
role: 'worker',
|
|
88
|
+
});
|
|
89
|
+
expect(
|
|
90
|
+
tasksFrontierResultSchema.parse({
|
|
91
|
+
rootId: 'T1',
|
|
92
|
+
role: 'worker',
|
|
93
|
+
groups: { ready: [], blocked: [], blockedBy: [] },
|
|
94
|
+
}).groups.ready,
|
|
95
|
+
).toEqual([]);
|
|
96
|
+
|
|
97
|
+
expect(
|
|
98
|
+
tasksWorkGraphAuditParamsSchema.parse({ rootId: 'T1', includeRelations: true, limit: 5 }),
|
|
99
|
+
).toEqual({
|
|
100
|
+
rootId: 'T1',
|
|
101
|
+
includeRelations: true,
|
|
102
|
+
limit: 5,
|
|
103
|
+
});
|
|
104
|
+
expect(
|
|
105
|
+
tasksWorkGraphAuditResultSchema.parse({
|
|
106
|
+
rootId: 'T1',
|
|
107
|
+
hierarchy: { valid: true, violations: [] },
|
|
108
|
+
traversal: {
|
|
109
|
+
rootId: 'T1',
|
|
110
|
+
direction: 'descendants',
|
|
111
|
+
nodes: [],
|
|
112
|
+
edges: [],
|
|
113
|
+
pageInfo: { hasMore: false },
|
|
114
|
+
},
|
|
115
|
+
frontier: { rootId: 'T1', groups: { ready: [], blocked: [], blockedBy: [] } },
|
|
116
|
+
rollup: {
|
|
117
|
+
rootId: 'T1',
|
|
118
|
+
direct: { total: 0, byStatus: {}, byType: {} },
|
|
119
|
+
subtree: { total: 0, byStatus: {}, byType: {} },
|
|
120
|
+
percentDenominator: { basis: 'subtree-total', total: 0, description: 'subtree total' },
|
|
121
|
+
percentages: { done: 0, active: 0, blocked: 0, pending: 0, cancelled: 0 },
|
|
122
|
+
staleProjection: false,
|
|
123
|
+
projectionMismatches: [],
|
|
124
|
+
},
|
|
125
|
+
relationEdges: { rootId: 'T1', direction: 'both', edges: [] },
|
|
126
|
+
}).hierarchy.valid,
|
|
127
|
+
).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('publishes operation metadata with explicit pagination params', () => {
|
|
131
|
+
for (const name of requiredOperations) {
|
|
132
|
+
const def = operation(name);
|
|
133
|
+
expect(def.gateway).toBe('query');
|
|
134
|
+
expect(def.domain).toBe('tasks');
|
|
135
|
+
expect(def.idempotent).toBe(true);
|
|
136
|
+
expect(def.params ?? []).not.toEqual([]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expect(operation('traverse').requiredParams).toEqual(['rootId', 'direction']);
|
|
140
|
+
expect(operation('tree').requiredParams).toEqual(['rootId']);
|
|
141
|
+
expect(operation('rollup').requiredParams).toEqual(['rootId']);
|
|
142
|
+
expect(operation('frontier').requiredParams).toEqual(['rootId']);
|
|
143
|
+
expect(operation('workgraph.audit').requiredParams).toEqual(['rootId']);
|
|
144
|
+
|
|
145
|
+
for (const name of ['traverse', 'tree', 'workgraph.audit'] as const) {
|
|
146
|
+
const params = operation(name).params?.map((param) => param.name) ?? [];
|
|
147
|
+
expect(params).toContain('cursor');
|
|
148
|
+
expect(params).toContain('limit');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
package/src/data-accessor.ts
CHANGED
|
@@ -119,6 +119,14 @@ export interface AcRow {
|
|
|
119
119
|
taskId: string;
|
|
120
120
|
/** 1-based ordinal — never reused per task (gaps remain on shrink). */
|
|
121
121
|
ordinal: number;
|
|
122
|
+
/** Typed completion criterion discriminator per ADR-088. */
|
|
123
|
+
kind: 'text' | 'child_task' | 'evidence_bound';
|
|
124
|
+
/** Stable per-task source key for idempotent criteria projection/upsert. */
|
|
125
|
+
sourceKey: string;
|
|
126
|
+
/** Optional child task target; only `kind='child_task'` may populate it. */
|
|
127
|
+
targetTaskId: string | null;
|
|
128
|
+
/** Compatibility projection owner (for example: legacy, direct, parent-child). */
|
|
129
|
+
projection: string;
|
|
122
130
|
/** The AC statement text. Structured gates are serialised as JSON. */
|
|
123
131
|
text: string;
|
|
124
132
|
/** ISO-8601 timestamp the row was created. */
|
|
@@ -129,6 +137,56 @@ export interface AcRow {
|
|
|
129
137
|
contentHash: string | null;
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
/** Machine-readable AC child-projection drift codes for doctor/audit output. */
|
|
141
|
+
export type AcProjectionAuditFindingCode =
|
|
142
|
+
| 'missing_child_task_row'
|
|
143
|
+
| 'extra_child_task_row'
|
|
144
|
+
| 'mismatched_child_task_row'
|
|
145
|
+
| 'stale_child_task_projection';
|
|
146
|
+
|
|
147
|
+
/** Dirty/clean status for an AC projection audit scan. */
|
|
148
|
+
export type AcProjectionAuditStatus = 'clean' | 'dirty';
|
|
149
|
+
|
|
150
|
+
/** Field-level child projection mismatch surfaced by doctor/audit callers. */
|
|
151
|
+
export interface AcProjectionAuditFinding {
|
|
152
|
+
/** Stable machine-readable finding code. */
|
|
153
|
+
code: AcProjectionAuditFindingCode;
|
|
154
|
+
/** Parent task whose AC rows were audited. */
|
|
155
|
+
parentId: string;
|
|
156
|
+
/** Direct child expected by WorkGraph containment, when applicable. */
|
|
157
|
+
childId?: string;
|
|
158
|
+
/** Existing AC row id involved in the finding, when applicable. */
|
|
159
|
+
acId?: string;
|
|
160
|
+
/** Compared row field, or `row` for whole-row missing/extra findings. */
|
|
161
|
+
field: 'row' | 'kind' | 'sourceKey' | 'targetTaskId' | 'projection' | 'text' | 'contentHash';
|
|
162
|
+
/** Expected canonical value. */
|
|
163
|
+
expected: string | null;
|
|
164
|
+
/** Actual observed value. */
|
|
165
|
+
actual: string | null;
|
|
166
|
+
/** True when this finding proves cached projection state is dirty/stale. */
|
|
167
|
+
dirty: true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Typed result returned by AC projection doctor/audit scanners. */
|
|
171
|
+
export interface AcProjectionAuditResult {
|
|
172
|
+
/** Parent task whose child_task projection rows were audited. */
|
|
173
|
+
parentId: string;
|
|
174
|
+
/** Clean when no findings were emitted, dirty otherwise. */
|
|
175
|
+
status: AcProjectionAuditStatus;
|
|
176
|
+
/** Boolean convenience flag for CLIs that render dirty state. */
|
|
177
|
+
dirty: boolean;
|
|
178
|
+
/** Number of direct children WorkGraph says should be projected. */
|
|
179
|
+
expectedRows: number;
|
|
180
|
+
/** Number of existing child_task projection rows observed on the parent. */
|
|
181
|
+
actualRows: number;
|
|
182
|
+
/** Stable sha256 over the expected child projection state. */
|
|
183
|
+
freshnessFingerprint: string;
|
|
184
|
+
/** True when at least one finding indicates stale/missing/extra projection state. */
|
|
185
|
+
staleProjection: boolean;
|
|
186
|
+
/** Typed findings suitable for JSON doctor/audit output. */
|
|
187
|
+
findings: readonly AcProjectionAuditFinding[];
|
|
188
|
+
}
|
|
189
|
+
|
|
132
190
|
/**
|
|
133
191
|
* A row of the `evidence_ac_bindings` table (T10503) — the M:N join between
|
|
134
192
|
* evidence atoms and acceptance criteria. Powers the AC-coverage gate
|
|
@@ -150,6 +208,26 @@ export interface AcBindingRow {
|
|
|
150
208
|
createdAt: string;
|
|
151
209
|
}
|
|
152
210
|
|
|
211
|
+
/** Query options for bounded reads from the append-only task audit log. @task T10594 */
|
|
212
|
+
export interface TaskAuditLogQuery {
|
|
213
|
+
taskIds?: readonly string[];
|
|
214
|
+
actions?: readonly string[];
|
|
215
|
+
since?: string;
|
|
216
|
+
limit?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** DataAccessor-facing shape of audit_log rows used by completion context packs. @task T10594 */
|
|
220
|
+
export interface TaskAuditLogRow {
|
|
221
|
+
id: string;
|
|
222
|
+
timestamp: string;
|
|
223
|
+
action: string;
|
|
224
|
+
taskId: string;
|
|
225
|
+
actor: string;
|
|
226
|
+
detailsJson: string | null;
|
|
227
|
+
beforeJson: string | null;
|
|
228
|
+
afterJson: string | null;
|
|
229
|
+
}
|
|
230
|
+
|
|
153
231
|
/**
|
|
154
232
|
* Subset of DataAccessor methods available inside a transaction callback.
|
|
155
233
|
* Write-only — reads use the outer accessor (snapshot isolation).
|
|
@@ -160,6 +238,8 @@ export interface TransactionAccessor {
|
|
|
160
238
|
removeSingleTask(taskId: string): Promise<void>;
|
|
161
239
|
setMetaValue(key: string, value: unknown): Promise<void>;
|
|
162
240
|
updateTaskFields(taskId: string, fields: TaskFieldUpdates): Promise<void>;
|
|
241
|
+
/** Get direct non-archived children inside the caller-owned transaction. @task T10590 */
|
|
242
|
+
getChildren(parentId: string): Promise<Task[]>;
|
|
163
243
|
appendLog(entry: Record<string, unknown>): Promise<void>;
|
|
164
244
|
/** Persist a relation row to task_relations. @task T9514 */
|
|
165
245
|
addRelation(
|
|
@@ -179,7 +259,17 @@ export interface TransactionAccessor {
|
|
|
179
259
|
* @task T10508
|
|
180
260
|
*/
|
|
181
261
|
insertAcRows(
|
|
182
|
-
rows: Array<{
|
|
262
|
+
rows: Array<{
|
|
263
|
+
id: string;
|
|
264
|
+
taskId: string;
|
|
265
|
+
ordinal: number;
|
|
266
|
+
text: string;
|
|
267
|
+
kind?: 'text' | 'child_task' | 'evidence_bound';
|
|
268
|
+
sourceKey?: string;
|
|
269
|
+
targetTaskId?: string | null;
|
|
270
|
+
projection?: string;
|
|
271
|
+
contentHash?: string | null;
|
|
272
|
+
}>,
|
|
183
273
|
): Promise<void>;
|
|
184
274
|
/**
|
|
185
275
|
* Read all AC rows for a task, ordered by ordinal ASC.
|
|
@@ -268,6 +358,9 @@ export interface DataAccessor {
|
|
|
268
358
|
/** Append an entry to the audit log. */
|
|
269
359
|
appendLog(entry: Record<string, unknown>): Promise<void>;
|
|
270
360
|
|
|
361
|
+
/** Query recent task audit rows by task id/action, newest first. @task T10594 */
|
|
362
|
+
queryAuditLog(query: TaskAuditLogQuery): Promise<TaskAuditLogRow[]>;
|
|
363
|
+
|
|
271
364
|
// ---- Lifecycle ----
|
|
272
365
|
|
|
273
366
|
/** Release any resources (close DB connections, etc.). */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DOCS_LIFECYCLE_STATUSES } from '../../operations/docs.js';
|
|
3
|
+
import { OPERATIONS } from '../operations-registry.js';
|
|
4
|
+
|
|
5
|
+
describe('docs.update registry flags (T10617)', () => {
|
|
6
|
+
it('exposes registry-driven dry-run and strict preflight CLI params', () => {
|
|
7
|
+
const updateOp = OPERATIONS.find(
|
|
8
|
+
(op) => op.domain === 'docs' && op.operation === 'update' && op.gateway === 'mutate',
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(updateOp).toBeDefined();
|
|
12
|
+
const dryRun = updateOp?.params?.find((param) => param.name === 'dryRun');
|
|
13
|
+
const strict = updateOp?.params?.find((param) => param.name === 'strict');
|
|
14
|
+
|
|
15
|
+
expect(dryRun).toMatchObject({
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
required: false,
|
|
18
|
+
cli: { flag: 'dry-run' },
|
|
19
|
+
});
|
|
20
|
+
expect(strict).toMatchObject({
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
required: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('derives lifecycle help and enum from the runtime docs.update status SSoT', () => {
|
|
27
|
+
const updateOp = OPERATIONS.find(
|
|
28
|
+
(op) => op.domain === 'docs' && op.operation === 'update' && op.gateway === 'mutate',
|
|
29
|
+
);
|
|
30
|
+
const status = updateOp?.params?.find((param) => param.name === 'status');
|
|
31
|
+
|
|
32
|
+
expect(status?.enum).toEqual(DOCS_LIFECYCLE_STATUSES);
|
|
33
|
+
expect(status?.description).toContain(`Valid: ${DOCS_LIFECYCLE_STATUSES.join('|')}.`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { OperationDef } from '../operation-def.js';
|
|
3
|
+
import { OPERATIONS } from '../operations-registry.js';
|
|
4
|
+
|
|
5
|
+
function opId(op: OperationDef): string {
|
|
6
|
+
return `${op.gateway}:${op.domain}:${op.operation}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function envelopeSnapshot(op: OperationDef | undefined) {
|
|
10
|
+
expect(op).toBeDefined();
|
|
11
|
+
return {
|
|
12
|
+
gateway: op?.gateway,
|
|
13
|
+
domain: op?.domain,
|
|
14
|
+
operation: op?.operation,
|
|
15
|
+
tier: op?.tier,
|
|
16
|
+
idempotent: op?.idempotent,
|
|
17
|
+
sessionRequired: op?.sessionRequired,
|
|
18
|
+
requiredParams: op?.requiredParams ?? [],
|
|
19
|
+
params: (op?.params ?? []).map((param) => ({
|
|
20
|
+
name: param.name,
|
|
21
|
+
type: param.type,
|
|
22
|
+
required: param.required,
|
|
23
|
+
cli: param.cli,
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getOp(
|
|
29
|
+
gateway: OperationDef['gateway'],
|
|
30
|
+
domain: string,
|
|
31
|
+
operation: string,
|
|
32
|
+
): OperationDef | undefined {
|
|
33
|
+
return OPERATIONS.find(
|
|
34
|
+
(op) => op.gateway === gateway && op.domain === domain && op.operation === operation,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('operation envelope registry snapshots (T10615)', () => {
|
|
39
|
+
it('keeps high-risk operation identities unique across gateway/domain/operation', () => {
|
|
40
|
+
const criticalIds = [
|
|
41
|
+
'query:tasks:show',
|
|
42
|
+
'query:tasks:workgraph.audit',
|
|
43
|
+
'mutate:tasks:saga.create',
|
|
44
|
+
'mutate:docs:update',
|
|
45
|
+
];
|
|
46
|
+
const seen = new Set(OPERATIONS.map(opId));
|
|
47
|
+
|
|
48
|
+
expect([...seen].filter((id) => criticalIds.includes(id)).sort()).toEqual(
|
|
49
|
+
[...criticalIds].sort(),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps requiredParams aligned with declared params for high-risk envelopes', () => {
|
|
54
|
+
const criticalOps = [
|
|
55
|
+
getOp('query', 'tasks', 'show'),
|
|
56
|
+
getOp('query', 'tasks', 'workgraph.audit'),
|
|
57
|
+
getOp('mutate', 'tasks', 'saga.create'),
|
|
58
|
+
getOp('mutate', 'docs', 'update'),
|
|
59
|
+
].filter((op): op is OperationDef => op !== undefined);
|
|
60
|
+
|
|
61
|
+
const offenders = criticalOps.flatMap((op) => {
|
|
62
|
+
const paramNames = new Set((op.params ?? []).map((param) => param.name));
|
|
63
|
+
return (op.requiredParams ?? [])
|
|
64
|
+
.filter((name) => !paramNames.has(name))
|
|
65
|
+
.map((missing) => `${opId(op)} missing declared param ${missing}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(offenders).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('snapshots high-risk operation envelopes consumed by CLI/domain dispatch adapters', () => {
|
|
72
|
+
const snapshots = {
|
|
73
|
+
'query:tasks:show': envelopeSnapshot(getOp('query', 'tasks', 'show')),
|
|
74
|
+
'query:tasks:workgraph.audit': envelopeSnapshot(getOp('query', 'tasks', 'workgraph.audit')),
|
|
75
|
+
'mutate:tasks:saga.create': envelopeSnapshot(getOp('mutate', 'tasks', 'saga.create')),
|
|
76
|
+
'mutate:docs:update': envelopeSnapshot(getOp('mutate', 'docs', 'update')),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
expect(snapshots).toMatchInlineSnapshot(`
|
|
80
|
+
{
|
|
81
|
+
"mutate:docs:update": {
|
|
82
|
+
"domain": "docs",
|
|
83
|
+
"gateway": "mutate",
|
|
84
|
+
"idempotent": false,
|
|
85
|
+
"operation": "update",
|
|
86
|
+
"params": [
|
|
87
|
+
{
|
|
88
|
+
"cli": {
|
|
89
|
+
"positional": true,
|
|
90
|
+
},
|
|
91
|
+
"name": "slug",
|
|
92
|
+
"required": true,
|
|
93
|
+
"type": "string",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"cli": undefined,
|
|
97
|
+
"name": "file",
|
|
98
|
+
"required": false,
|
|
99
|
+
"type": "string",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"cli": {
|
|
103
|
+
"flag": "allow-external",
|
|
104
|
+
},
|
|
105
|
+
"name": "allowExternal",
|
|
106
|
+
"required": false,
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"cli": undefined,
|
|
111
|
+
"name": "content",
|
|
112
|
+
"required": false,
|
|
113
|
+
"type": "string",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"cli": undefined,
|
|
117
|
+
"name": "message",
|
|
118
|
+
"required": false,
|
|
119
|
+
"type": "string",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"cli": undefined,
|
|
123
|
+
"name": "status",
|
|
124
|
+
"required": false,
|
|
125
|
+
"type": "string",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"cli": {
|
|
129
|
+
"flag": "dry-run",
|
|
130
|
+
},
|
|
131
|
+
"name": "dryRun",
|
|
132
|
+
"required": false,
|
|
133
|
+
"type": "boolean",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"cli": undefined,
|
|
137
|
+
"name": "strict",
|
|
138
|
+
"required": false,
|
|
139
|
+
"type": "boolean",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"cli": {
|
|
143
|
+
"flag": "attached-by",
|
|
144
|
+
},
|
|
145
|
+
"name": "attachedBy",
|
|
146
|
+
"required": false,
|
|
147
|
+
"type": "string",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
"requiredParams": [
|
|
151
|
+
"slug",
|
|
152
|
+
],
|
|
153
|
+
"sessionRequired": false,
|
|
154
|
+
"tier": 1,
|
|
155
|
+
},
|
|
156
|
+
"mutate:tasks:saga.create": {
|
|
157
|
+
"domain": "tasks",
|
|
158
|
+
"gateway": "mutate",
|
|
159
|
+
"idempotent": false,
|
|
160
|
+
"operation": "saga.create",
|
|
161
|
+
"params": [
|
|
162
|
+
{
|
|
163
|
+
"cli": {
|
|
164
|
+
"flag": "title",
|
|
165
|
+
},
|
|
166
|
+
"name": "title",
|
|
167
|
+
"required": true,
|
|
168
|
+
"type": "string",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"cli": {
|
|
172
|
+
"flag": "description",
|
|
173
|
+
"short": "-d",
|
|
174
|
+
},
|
|
175
|
+
"name": "description",
|
|
176
|
+
"required": false,
|
|
177
|
+
"type": "string",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"cli": {
|
|
181
|
+
"flag": "acceptance",
|
|
182
|
+
},
|
|
183
|
+
"name": "acceptance",
|
|
184
|
+
"required": false,
|
|
185
|
+
"type": "array",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"cli": {
|
|
189
|
+
"flag": "dry-run",
|
|
190
|
+
},
|
|
191
|
+
"name": "dryRun",
|
|
192
|
+
"required": false,
|
|
193
|
+
"type": "boolean",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
"requiredParams": [
|
|
197
|
+
"title",
|
|
198
|
+
],
|
|
199
|
+
"sessionRequired": false,
|
|
200
|
+
"tier": 0,
|
|
201
|
+
},
|
|
202
|
+
"query:tasks:show": {
|
|
203
|
+
"domain": "tasks",
|
|
204
|
+
"gateway": "query",
|
|
205
|
+
"idempotent": true,
|
|
206
|
+
"operation": "show",
|
|
207
|
+
"params": [
|
|
208
|
+
{
|
|
209
|
+
"cli": {
|
|
210
|
+
"positional": true,
|
|
211
|
+
},
|
|
212
|
+
"name": "taskId",
|
|
213
|
+
"required": true,
|
|
214
|
+
"type": "string",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"cli": undefined,
|
|
218
|
+
"name": "history",
|
|
219
|
+
"required": false,
|
|
220
|
+
"type": "boolean",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"cli": undefined,
|
|
224
|
+
"name": "ivtr-history",
|
|
225
|
+
"required": false,
|
|
226
|
+
"type": "boolean",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
"requiredParams": [
|
|
230
|
+
"taskId",
|
|
231
|
+
],
|
|
232
|
+
"sessionRequired": false,
|
|
233
|
+
"tier": 0,
|
|
234
|
+
},
|
|
235
|
+
"query:tasks:workgraph.audit": {
|
|
236
|
+
"domain": "tasks",
|
|
237
|
+
"gateway": "query",
|
|
238
|
+
"idempotent": true,
|
|
239
|
+
"operation": "workgraph.audit",
|
|
240
|
+
"params": [
|
|
241
|
+
{
|
|
242
|
+
"cli": {
|
|
243
|
+
"positional": true,
|
|
244
|
+
},
|
|
245
|
+
"name": "rootId",
|
|
246
|
+
"required": true,
|
|
247
|
+
"type": "string",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"cli": undefined,
|
|
251
|
+
"name": "maxDepth",
|
|
252
|
+
"required": false,
|
|
253
|
+
"type": "number",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"cli": undefined,
|
|
257
|
+
"name": "includeRelations",
|
|
258
|
+
"required": false,
|
|
259
|
+
"type": "boolean",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"cli": undefined,
|
|
263
|
+
"name": "cursor",
|
|
264
|
+
"required": false,
|
|
265
|
+
"type": "string",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"cli": undefined,
|
|
269
|
+
"name": "limit",
|
|
270
|
+
"required": false,
|
|
271
|
+
"type": "number",
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
"requiredParams": [
|
|
275
|
+
"rootId",
|
|
276
|
+
],
|
|
277
|
+
"sessionRequired": false,
|
|
278
|
+
"tier": 1,
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
`);
|
|
282
|
+
});
|
|
283
|
+
});
|