@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.
- 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.d.ts +24 -0
- package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
- package/dist/__tests__/satisfies-atom.test.js +320 -0
- package/dist/__tests__/satisfies-atom.test.js.map +1 -0
- package/dist/__tests__/validator-types.test.d.ts +13 -0
- package/dist/__tests__/validator-types.test.d.ts.map +1 -0
- package/dist/__tests__/validator-types.test.js +293 -0
- package/dist/__tests__/validator-types.test.js.map +1 -0
- 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/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/data-accessor.d.ts +194 -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 +91 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +162 -1
- package/dist/evidence-atom-schema.js.map +1 -1
- package/dist/exit-codes.d.ts +41 -1
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +40 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +17 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- 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 +617 -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 +52 -5
- 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/validator/index.d.ts +375 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +162 -0
- package/dist/validator/index.js.map +1 -0
- 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 +383 -0
- package/src/__tests__/validator-types.test.ts +359 -0
- package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
- package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
- package/src/config.ts +58 -0
- package/src/data-accessor.ts +213 -0
- 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 +188 -1
- package/src/exit-codes.ts +42 -0
- package/src/index.ts +230 -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 +662 -15
- package/src/task-record.ts +16 -0
- package/src/task.ts +54 -5
- package/src/tasks.ts +558 -0
- package/src/validator/index.ts +385 -0
- package/src/workgraph.ts +1153 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level and runtime shape tests for T10510 Validator role contracts.
|
|
3
|
+
*
|
|
4
|
+
* Covers the AgentRole enum extension and the four envelope types
|
|
5
|
+
* (`ValidatorFinding`, `ValidatorAttestation`, `ValidatorRejection`,
|
|
6
|
+
* `ValidatorVerdict`) plus their Zod schemas and type guards.
|
|
7
|
+
*
|
|
8
|
+
* @task T10510
|
|
9
|
+
* @epic T10383 (E-VALIDATOR-ROLE)
|
|
10
|
+
* @saga T10377 (SG-IVTR-AC-BINDING)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
AGENT_ROLES,
|
|
16
|
+
type AgentRole,
|
|
17
|
+
isAgentRole,
|
|
18
|
+
isValidatorAttestation,
|
|
19
|
+
isValidatorRejection,
|
|
20
|
+
isValidatorVerdict,
|
|
21
|
+
VALIDATOR_ID_REGEX,
|
|
22
|
+
type ValidatorAttestation,
|
|
23
|
+
type ValidatorFinding,
|
|
24
|
+
type ValidatorRejection,
|
|
25
|
+
type ValidatorVerdict,
|
|
26
|
+
validatorAttestationSchema,
|
|
27
|
+
validatorFindingSchema,
|
|
28
|
+
validatorRejectionSchema,
|
|
29
|
+
validatorVerdictSchema,
|
|
30
|
+
} from '../validator/index.js';
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Test fixtures
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const NOW = '2026-05-24T12:00:00Z';
|
|
37
|
+
|
|
38
|
+
const passFinding: ValidatorFinding = {
|
|
39
|
+
acId: '11111111-1111-4111-8111-111111111111',
|
|
40
|
+
status: 'pass',
|
|
41
|
+
reasoning: 'AC satisfied — implementation matches spec',
|
|
42
|
+
checkedAt: NOW,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const failFinding: ValidatorFinding = {
|
|
46
|
+
acId: '22222222-2222-4222-8222-222222222222',
|
|
47
|
+
status: 'fail',
|
|
48
|
+
reasoning: 'Missing error handling for the null branch',
|
|
49
|
+
evidenceRefs: ['src/foo.ts:42', 'tests/foo.test.ts:17'],
|
|
50
|
+
checkedAt: NOW,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const inconclusiveFinding: ValidatorFinding = {
|
|
54
|
+
acId: '33333333-3333-4333-8333-333333333333',
|
|
55
|
+
status: 'inconclusive',
|
|
56
|
+
reasoning: 'Cannot reproduce the claimed test — file not present',
|
|
57
|
+
checkedAt: NOW,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// AgentRole enum
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
describe('AgentRole enum (T10510)', () => {
|
|
65
|
+
it('contains the four canonical roles', () => {
|
|
66
|
+
expect(AGENT_ROLES).toEqual(['orchestrator', 'lead', 'worker', 'validator']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('AGENT_ROLES is a readonly tuple of length 4', () => {
|
|
70
|
+
// `as const` is a compile-time readonly marker — there is no runtime
|
|
71
|
+
// freeze. The TS type guard below ensures mutation attempts are rejected
|
|
72
|
+
// by the compiler; runtime length is the structural invariant.
|
|
73
|
+
expect(AGENT_ROLES.length).toBe(4);
|
|
74
|
+
// @ts-expect-error — readonly tuple disallows .push at the type level.
|
|
75
|
+
AGENT_ROLES.push;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('AgentRole type compiles for all four values', () => {
|
|
79
|
+
const roles: AgentRole[] = ['orchestrator', 'lead', 'worker', 'validator'];
|
|
80
|
+
expect(roles).toHaveLength(4);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('isAgentRole accepts canonical values', () => {
|
|
84
|
+
for (const r of AGENT_ROLES) {
|
|
85
|
+
expect(isAgentRole(r)).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('isAgentRole rejects unknown strings and non-strings', () => {
|
|
90
|
+
expect(isAgentRole('subagent')).toBe(false);
|
|
91
|
+
expect(isAgentRole('Validator')).toBe(false);
|
|
92
|
+
expect(isAgentRole('')).toBe(false);
|
|
93
|
+
expect(isAgentRole(undefined)).toBe(false);
|
|
94
|
+
expect(isAgentRole(null)).toBe(false);
|
|
95
|
+
expect(isAgentRole(42)).toBe(false);
|
|
96
|
+
expect(isAgentRole({})).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// VALIDATOR_ID_REGEX
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
describe('VALIDATOR_ID_REGEX (T10510)', () => {
|
|
105
|
+
it('accepts canonical validator agentIds', () => {
|
|
106
|
+
expect(VALIDATOR_ID_REGEX.test('validator-prime')).toBe(true);
|
|
107
|
+
expect(VALIDATOR_ID_REGEX.test('validator-sec-001')).toBe(true);
|
|
108
|
+
expect(VALIDATOR_ID_REGEX.test('validator-a')).toBe(true);
|
|
109
|
+
expect(VALIDATOR_ID_REGEX.test('validator-9-test')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects malformed validator agentIds', () => {
|
|
113
|
+
expect(VALIDATOR_ID_REGEX.test('validator-')).toBe(false);
|
|
114
|
+
expect(VALIDATOR_ID_REGEX.test('Validator-prime')).toBe(false);
|
|
115
|
+
expect(VALIDATOR_ID_REGEX.test('validator-Prime')).toBe(false);
|
|
116
|
+
expect(VALIDATOR_ID_REGEX.test('worker-prime')).toBe(false);
|
|
117
|
+
expect(VALIDATOR_ID_REGEX.test('validator-_x')).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// ValidatorFinding
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
describe('ValidatorFinding (T10510)', () => {
|
|
126
|
+
it('accepts pass / fail / inconclusive', () => {
|
|
127
|
+
expect(validatorFindingSchema.safeParse(passFinding).success).toBe(true);
|
|
128
|
+
expect(validatorFindingSchema.safeParse(failFinding).success).toBe(true);
|
|
129
|
+
expect(validatorFindingSchema.safeParse(inconclusiveFinding).success).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects empty acId', () => {
|
|
133
|
+
const bad = { ...passFinding, acId: '' };
|
|
134
|
+
expect(validatorFindingSchema.safeParse(bad).success).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects empty reasoning', () => {
|
|
138
|
+
const bad = { ...passFinding, reasoning: '' };
|
|
139
|
+
expect(validatorFindingSchema.safeParse(bad).success).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects unknown status value', () => {
|
|
143
|
+
const bad = { ...passFinding, status: 'warn' };
|
|
144
|
+
expect(validatorFindingSchema.safeParse(bad).success).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('accepts optional evidenceRefs array', () => {
|
|
148
|
+
const withRefs = { ...passFinding, evidenceRefs: ['a', 'b'] };
|
|
149
|
+
expect(validatorFindingSchema.safeParse(withRefs).success).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// ValidatorAttestation
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
describe('ValidatorAttestation (T10510)', () => {
|
|
158
|
+
const baseAttestation: ValidatorAttestation = {
|
|
159
|
+
verdict: 'attest',
|
|
160
|
+
taskId: 'T10510',
|
|
161
|
+
validatorId: 'validator-prime',
|
|
162
|
+
findings: [passFinding],
|
|
163
|
+
attestedAt: NOW,
|
|
164
|
+
schemaVersion: '1',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
it('accepts a well-formed attestation with all passes', () => {
|
|
168
|
+
expect(validatorAttestationSchema.safeParse(baseAttestation).success).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('accepts optional summary', () => {
|
|
172
|
+
const withSummary = { ...baseAttestation, summary: 'All ACs verified' };
|
|
173
|
+
expect(validatorAttestationSchema.safeParse(withSummary).success).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('accepts multiple pass findings', () => {
|
|
177
|
+
const multi = {
|
|
178
|
+
...baseAttestation,
|
|
179
|
+
findings: [passFinding, { ...passFinding, acId: '44444444-4444-4444-8444-444444444444' }],
|
|
180
|
+
};
|
|
181
|
+
expect(validatorAttestationSchema.safeParse(multi).success).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('REJECTS attestation containing any fail finding', () => {
|
|
185
|
+
const mixed = { ...baseAttestation, findings: [passFinding, failFinding] };
|
|
186
|
+
const r = validatorAttestationSchema.safeParse(mixed);
|
|
187
|
+
expect(r.success).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('REJECTS attestation containing inconclusive finding', () => {
|
|
191
|
+
const mixed = { ...baseAttestation, findings: [passFinding, inconclusiveFinding] };
|
|
192
|
+
expect(validatorAttestationSchema.safeParse(mixed).success).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('REJECTS empty findings array', () => {
|
|
196
|
+
const empty = { ...baseAttestation, findings: [] };
|
|
197
|
+
expect(validatorAttestationSchema.safeParse(empty).success).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('REJECTS malformed validatorId', () => {
|
|
201
|
+
const bad = { ...baseAttestation, validatorId: 'worker-prime' };
|
|
202
|
+
expect(validatorAttestationSchema.safeParse(bad).success).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('REJECTS wrong verdict discriminant', () => {
|
|
206
|
+
const bad = { ...baseAttestation, verdict: 'reject' };
|
|
207
|
+
expect(validatorAttestationSchema.safeParse(bad).success).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('REJECTS wrong schemaVersion', () => {
|
|
211
|
+
const bad = { ...baseAttestation, schemaVersion: '2' };
|
|
212
|
+
expect(validatorAttestationSchema.safeParse(bad).success).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// ValidatorRejection
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
describe('ValidatorRejection (T10510)', () => {
|
|
221
|
+
const baseRejection: ValidatorRejection = {
|
|
222
|
+
verdict: 'reject',
|
|
223
|
+
taskId: 'T10510',
|
|
224
|
+
validatorId: 'validator-sec-001',
|
|
225
|
+
findings: [failFinding],
|
|
226
|
+
summary: 'AC#2 fails — null branch unhandled',
|
|
227
|
+
rejectedAt: NOW,
|
|
228
|
+
schemaVersion: '1',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
it('accepts a well-formed rejection with a fail finding', () => {
|
|
232
|
+
expect(validatorRejectionSchema.safeParse(baseRejection).success).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('accepts a rejection with only an inconclusive finding', () => {
|
|
236
|
+
const inc = { ...baseRejection, findings: [inconclusiveFinding] };
|
|
237
|
+
expect(validatorRejectionSchema.safeParse(inc).success).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('accepts mixed pass + fail findings', () => {
|
|
241
|
+
const mixed = { ...baseRejection, findings: [passFinding, failFinding] };
|
|
242
|
+
expect(validatorRejectionSchema.safeParse(mixed).success).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('REJECTS rejection where every finding is pass', () => {
|
|
246
|
+
const allPass = { ...baseRejection, findings: [passFinding, passFinding] };
|
|
247
|
+
const r = validatorRejectionSchema.safeParse(allPass);
|
|
248
|
+
expect(r.success).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('REJECTS empty findings array', () => {
|
|
252
|
+
const empty = { ...baseRejection, findings: [] };
|
|
253
|
+
expect(validatorRejectionSchema.safeParse(empty).success).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('REJECTS empty summary', () => {
|
|
257
|
+
const bad = { ...baseRejection, summary: '' };
|
|
258
|
+
expect(validatorRejectionSchema.safeParse(bad).success).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('accepts optional remediationHints', () => {
|
|
262
|
+
const withHints = {
|
|
263
|
+
...baseRejection,
|
|
264
|
+
remediationHints: ['Add null guard to foo()', 'Add regression test'],
|
|
265
|
+
};
|
|
266
|
+
expect(validatorRejectionSchema.safeParse(withHints).success).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// ValidatorVerdict discriminated union
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
describe('ValidatorVerdict (T10510)', () => {
|
|
275
|
+
const attestation: ValidatorAttestation = {
|
|
276
|
+
verdict: 'attest',
|
|
277
|
+
taskId: 'T10510',
|
|
278
|
+
validatorId: 'validator-prime',
|
|
279
|
+
findings: [passFinding],
|
|
280
|
+
attestedAt: NOW,
|
|
281
|
+
schemaVersion: '1',
|
|
282
|
+
};
|
|
283
|
+
const rejection: ValidatorRejection = {
|
|
284
|
+
verdict: 'reject',
|
|
285
|
+
taskId: 'T10510',
|
|
286
|
+
validatorId: 'validator-prime',
|
|
287
|
+
findings: [failFinding],
|
|
288
|
+
summary: 'fail',
|
|
289
|
+
rejectedAt: NOW,
|
|
290
|
+
schemaVersion: '1',
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
it('schema accepts both attest and reject envelopes', () => {
|
|
294
|
+
expect(validatorVerdictSchema.safeParse(attestation).success).toBe(true);
|
|
295
|
+
expect(validatorVerdictSchema.safeParse(rejection).success).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('schema rejects envelope with unknown verdict', () => {
|
|
299
|
+
const bad = { ...attestation, verdict: 'maybe' };
|
|
300
|
+
expect(validatorVerdictSchema.safeParse(bad).success).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('discriminant narrows in TypeScript at compile time', () => {
|
|
304
|
+
const handle = (v: ValidatorVerdict): string => {
|
|
305
|
+
if (v.verdict === 'attest') {
|
|
306
|
+
// TS narrows to ValidatorAttestation here
|
|
307
|
+
return `attested ${v.attestedAt}`;
|
|
308
|
+
}
|
|
309
|
+
// TS narrows to ValidatorRejection here
|
|
310
|
+
return `rejected ${v.rejectedAt} — ${v.summary}`;
|
|
311
|
+
};
|
|
312
|
+
expect(handle(attestation)).toContain('attested');
|
|
313
|
+
expect(handle(rejection)).toContain('rejected');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ============================================================================
|
|
318
|
+
// Type guards
|
|
319
|
+
// ============================================================================
|
|
320
|
+
|
|
321
|
+
describe('Validator type guards (T10510)', () => {
|
|
322
|
+
const attestation: ValidatorAttestation = {
|
|
323
|
+
verdict: 'attest',
|
|
324
|
+
taskId: 'T10510',
|
|
325
|
+
validatorId: 'validator-prime',
|
|
326
|
+
findings: [passFinding],
|
|
327
|
+
attestedAt: NOW,
|
|
328
|
+
schemaVersion: '1',
|
|
329
|
+
};
|
|
330
|
+
const rejection: ValidatorRejection = {
|
|
331
|
+
verdict: 'reject',
|
|
332
|
+
taskId: 'T10510',
|
|
333
|
+
validatorId: 'validator-prime',
|
|
334
|
+
findings: [failFinding],
|
|
335
|
+
summary: 'fail',
|
|
336
|
+
rejectedAt: NOW,
|
|
337
|
+
schemaVersion: '1',
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
it('isValidatorAttestation returns true only for attestations', () => {
|
|
341
|
+
expect(isValidatorAttestation(attestation)).toBe(true);
|
|
342
|
+
expect(isValidatorAttestation(rejection)).toBe(false);
|
|
343
|
+
expect(isValidatorAttestation(null)).toBe(false);
|
|
344
|
+
expect(isValidatorAttestation({})).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('isValidatorRejection returns true only for rejections', () => {
|
|
348
|
+
expect(isValidatorRejection(rejection)).toBe(true);
|
|
349
|
+
expect(isValidatorRejection(attestation)).toBe(false);
|
|
350
|
+
expect(isValidatorRejection(null)).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('isValidatorVerdict returns true for both shapes', () => {
|
|
354
|
+
expect(isValidatorVerdict(attestation)).toBe(true);
|
|
355
|
+
expect(isValidatorVerdict(rejection)).toBe(true);
|
|
356
|
+
expect(isValidatorVerdict({ verdict: 'attest' })).toBe(false);
|
|
357
|
+
expect(isValidatorVerdict(null)).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for bounded WorkGraph context packs, readiness, and scaffold envelopes.
|
|
3
|
+
*
|
|
4
|
+
* @task T10609
|
|
5
|
+
* @saga T10538
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
workGraphContextPackParamsSchema,
|
|
11
|
+
workGraphContextPackSchema,
|
|
12
|
+
workGraphReadinessParamsSchema,
|
|
13
|
+
workGraphReadinessResultSchema,
|
|
14
|
+
workGraphScaffoldApplyParamsSchema,
|
|
15
|
+
workGraphScaffoldApplyResultSchema,
|
|
16
|
+
workGraphScaffoldValidateParamsSchema,
|
|
17
|
+
workGraphScaffoldValidateResultSchema,
|
|
18
|
+
workGraphSliceParamsSchema,
|
|
19
|
+
workGraphSliceSchema,
|
|
20
|
+
} from '../workgraph.js';
|
|
21
|
+
|
|
22
|
+
const taskNode = {
|
|
23
|
+
id: 'T10609',
|
|
24
|
+
type: 'task',
|
|
25
|
+
title: 'Add contracts',
|
|
26
|
+
status: 'pending',
|
|
27
|
+
priority: 'high',
|
|
28
|
+
parentId: 'T10545',
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const emptyPageInfo = { hasMore: false } as const;
|
|
32
|
+
|
|
33
|
+
const readyGroups = {
|
|
34
|
+
ready: [
|
|
35
|
+
{
|
|
36
|
+
...taskNode,
|
|
37
|
+
role: 'worker',
|
|
38
|
+
dependencyBlockers: [],
|
|
39
|
+
gateBlockers: [],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
blocked: [],
|
|
43
|
+
blockedBy: [],
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
describe('WorkGraph bounded context and scaffold contracts', () => {
|
|
47
|
+
it('validates bounded context packs with graph slices, relation edges, readiness, budget, and omissions', () => {
|
|
48
|
+
expect(
|
|
49
|
+
workGraphContextPackParamsSchema.parse({
|
|
50
|
+
rootId: 'T10545',
|
|
51
|
+
tokenBudget: 1500,
|
|
52
|
+
includeRelations: true,
|
|
53
|
+
includeReadiness: true,
|
|
54
|
+
includeRollup: true,
|
|
55
|
+
limit: 25,
|
|
56
|
+
}),
|
|
57
|
+
).toEqual({
|
|
58
|
+
rootId: 'T10545',
|
|
59
|
+
tokenBudget: 1500,
|
|
60
|
+
includeRelations: true,
|
|
61
|
+
includeReadiness: true,
|
|
62
|
+
includeRollup: true,
|
|
63
|
+
limit: 25,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const parsed = workGraphContextPackSchema.parse({
|
|
67
|
+
rootId: 'T10545',
|
|
68
|
+
generatedAt: '2026-05-26T10:00:00.000Z',
|
|
69
|
+
budget: {
|
|
70
|
+
tokenBudget: 1500,
|
|
71
|
+
estimatedTokens: 1200,
|
|
72
|
+
remainingTokens: 300,
|
|
73
|
+
truncated: true,
|
|
74
|
+
},
|
|
75
|
+
slice: {
|
|
76
|
+
rootId: 'T10545',
|
|
77
|
+
direction: 'descendants',
|
|
78
|
+
nodes: [taskNode],
|
|
79
|
+
edges: [{ fromId: 'T10545', toId: 'T10609', kind: 'contains' }],
|
|
80
|
+
pageInfo: { nextCursor: '0002:T10610', hasMore: true },
|
|
81
|
+
omissions: [
|
|
82
|
+
{
|
|
83
|
+
path: 'slice.nodes[25:]',
|
|
84
|
+
reason: 'budget_exceeded',
|
|
85
|
+
message: 'Additional nodes omitted to stay under token budget',
|
|
86
|
+
estimatedTokens: 400,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
relationEdges: {
|
|
91
|
+
rootId: 'T10609',
|
|
92
|
+
direction: 'both',
|
|
93
|
+
edges: [
|
|
94
|
+
{
|
|
95
|
+
source: 'dependency',
|
|
96
|
+
fromId: 'T10609',
|
|
97
|
+
toId: 'T10608',
|
|
98
|
+
kind: 'depends_on',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
source: 'relation',
|
|
102
|
+
fromId: 'T10538',
|
|
103
|
+
toId: 'T10545',
|
|
104
|
+
kind: 'groups',
|
|
105
|
+
relationType: 'groups',
|
|
106
|
+
reason: 'Saga membership',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
readiness: {
|
|
111
|
+
rootId: 'T10545',
|
|
112
|
+
role: 'worker',
|
|
113
|
+
ready: true,
|
|
114
|
+
warnings: [],
|
|
115
|
+
groups: readyGroups,
|
|
116
|
+
},
|
|
117
|
+
rollup: {
|
|
118
|
+
rootId: 'T10545',
|
|
119
|
+
direct: { total: 1, byStatus: { pending: 1 }, byType: { task: 1 } },
|
|
120
|
+
subtree: { total: 1, byStatus: { pending: 1 }, byType: { task: 1 } },
|
|
121
|
+
percentDenominator: { basis: 'subtree-total', total: 1, description: 'subtree total' },
|
|
122
|
+
percentages: { done: 0, active: 0, blocked: 0, pending: 100, cancelled: 0 },
|
|
123
|
+
staleProjection: false,
|
|
124
|
+
projectionMismatches: [],
|
|
125
|
+
},
|
|
126
|
+
omissions: [
|
|
127
|
+
{ path: 'rollup.history', reason: 'not_requested', message: 'History not requested' },
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(parsed.budget.truncated).toBe(true);
|
|
132
|
+
expect(parsed.relationEdges?.edges.map((edge) => edge.source)).toEqual([
|
|
133
|
+
'dependency',
|
|
134
|
+
'relation',
|
|
135
|
+
]);
|
|
136
|
+
expect(parsed.readiness?.groups.ready[0]?.id).toBe('T10609');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('validates graph slice and readiness envelopes independently', () => {
|
|
140
|
+
expect(
|
|
141
|
+
workGraphSliceParamsSchema.parse({
|
|
142
|
+
rootId: 'T10545',
|
|
143
|
+
direction: 'descendants',
|
|
144
|
+
maxDepth: 2,
|
|
145
|
+
includeRelations: true,
|
|
146
|
+
cursor: '0001:T10609',
|
|
147
|
+
limit: 50,
|
|
148
|
+
}),
|
|
149
|
+
).toEqual({
|
|
150
|
+
rootId: 'T10545',
|
|
151
|
+
direction: 'descendants',
|
|
152
|
+
maxDepth: 2,
|
|
153
|
+
includeRelations: true,
|
|
154
|
+
cursor: '0001:T10609',
|
|
155
|
+
limit: 50,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(
|
|
159
|
+
workGraphSliceSchema.parse({
|
|
160
|
+
rootId: 'T10545',
|
|
161
|
+
direction: 'descendants',
|
|
162
|
+
nodes: [taskNode],
|
|
163
|
+
edges: [{ fromId: 'T10545', toId: 'T10609', kind: 'contains' }],
|
|
164
|
+
pageInfo: emptyPageInfo,
|
|
165
|
+
}).nodes,
|
|
166
|
+
).toHaveLength(1);
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
workGraphReadinessParamsSchema.parse({
|
|
170
|
+
rootId: 'T10545',
|
|
171
|
+
role: 'worker',
|
|
172
|
+
includeGateBlockers: true,
|
|
173
|
+
}),
|
|
174
|
+
).toEqual({ rootId: 'T10545', role: 'worker', includeGateBlockers: true });
|
|
175
|
+
|
|
176
|
+
expect(
|
|
177
|
+
workGraphReadinessResultSchema.parse({
|
|
178
|
+
rootId: 'T10545',
|
|
179
|
+
role: 'worker',
|
|
180
|
+
ready: true,
|
|
181
|
+
warnings: [],
|
|
182
|
+
groups: readyGroups,
|
|
183
|
+
}).ready,
|
|
184
|
+
).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('validates scaffold validate/apply contracts and hierarchy diagnostics', () => {
|
|
188
|
+
const nodes = [
|
|
189
|
+
{ id: 'T10545', type: 'epic' },
|
|
190
|
+
{ id: 'T10609', type: 'task', parentId: 'T10545' },
|
|
191
|
+
] as const;
|
|
192
|
+
const edges = [
|
|
193
|
+
{
|
|
194
|
+
source: 'dependency',
|
|
195
|
+
fromId: 'T10609',
|
|
196
|
+
toId: 'T10608',
|
|
197
|
+
kind: 'depends_on',
|
|
198
|
+
},
|
|
199
|
+
] as const;
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
workGraphScaffoldValidateParamsSchema.parse({ rootId: 'T10545', nodes, edges, dryRun: true }),
|
|
203
|
+
).toEqual({ rootId: 'T10545', nodes, edges, dryRun: true });
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
workGraphScaffoldValidateResultSchema.parse({
|
|
207
|
+
rootId: 'T10545',
|
|
208
|
+
valid: true,
|
|
209
|
+
dryRun: true,
|
|
210
|
+
issues: [{ code: 'W_EXISTING_NODE', message: 'Node already exists', severity: 'warning' }],
|
|
211
|
+
hierarchy: { valid: true, violations: [] },
|
|
212
|
+
}).valid,
|
|
213
|
+
).toBe(true);
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
workGraphScaffoldApplyParamsSchema.parse({ rootId: 'T10545', nodes, edges, apply: true }),
|
|
217
|
+
).toEqual({ rootId: 'T10545', nodes, edges, apply: true });
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
workGraphScaffoldApplyResultSchema.parse({
|
|
221
|
+
rootId: 'T10545',
|
|
222
|
+
valid: true,
|
|
223
|
+
dryRun: false,
|
|
224
|
+
applied: true,
|
|
225
|
+
nodesChanged: 2,
|
|
226
|
+
edgesChanged: 1,
|
|
227
|
+
issues: [],
|
|
228
|
+
hierarchy: { valid: true, violations: [] },
|
|
229
|
+
}).edgesChanged,
|
|
230
|
+
).toBe(1);
|
|
231
|
+
});
|
|
232
|
+
});
|