@cleocode/contracts 2026.5.121 → 2026.5.122
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__/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 +302 -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/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/data-accessor.d.ts +117 -0
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.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 +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/operations/tasks.d.ts +49 -0
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js.map +1 -1
- package/dist/task.d.ts +38 -0
- package/dist/task.d.ts.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/package.json +2 -2
- package/src/__tests__/satisfies-atom.test.ts +363 -0
- package/src/__tests__/validator-types.test.ts +359 -0
- package/src/config.ts +58 -0
- package/src/data-accessor.ts +120 -0
- package/src/evidence-atom-schema.ts +188 -1
- package/src/exit-codes.ts +42 -0
- package/src/index.ts +37 -0
- package/src/operations/tasks.ts +50 -0
- package/src/task.ts +39 -0
- package/src/validator/index.ts +385 -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
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -530,6 +530,50 @@ export interface DecisionsConfig {
|
|
|
530
530
|
validatorConfidenceThreshold?: number;
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Operating mode for the Lead-tier wave roll-up
|
|
535
|
+
* (`packages/core/src/orchestration/lead-rollup.ts`).
|
|
536
|
+
*
|
|
537
|
+
* - `'passive'` — Default, backward-compatible behaviour. `rollupWaveStatus` /
|
|
538
|
+
* `rollupEpicStatus` compute the rollup contract from manifest +
|
|
539
|
+
* verification rows only. The Lead reads the result and decides what to do
|
|
540
|
+
* externally.
|
|
541
|
+
* - `'active'` — Enables the Lead↔Worker Max-N loop scaffolded by T10383
|
|
542
|
+
* (E-VALIDATOR-ROLE). The rollup will additionally surface retry-eligible
|
|
543
|
+
* workers and emit Lead-initiated retry signals. The wiring of retries
|
|
544
|
+
* themselves lives in T10512 — this flag is the gate.
|
|
545
|
+
* - `'auto'` — Reserved for future heuristic selection (e.g. switch to
|
|
546
|
+
* active mode iff the epic has ≥N pending workers). Treated as `'passive'`
|
|
547
|
+
* at runtime until the heuristic ships.
|
|
548
|
+
*
|
|
549
|
+
* @task T10513
|
|
550
|
+
* @saga T10377
|
|
551
|
+
* @adr ADR-070
|
|
552
|
+
*/
|
|
553
|
+
export type LeadRollupMode = 'passive' | 'active' | 'auto';
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Lead-tier wave roll-up configuration.
|
|
557
|
+
*
|
|
558
|
+
* Single-key config block gating the Lead↔Worker Max-N loop introduced by
|
|
559
|
+
* SG-IVTR-AC-BINDING (T10377) and council action #9 of E-VALIDATOR-ROLE
|
|
560
|
+
* (T10383). The whole point of this block is to keep the existing
|
|
561
|
+
* `rollupWaveStatus` / `rollupEpicStatus` function signatures unchanged —
|
|
562
|
+
* callers pick up new behaviour by flipping the config key, never by
|
|
563
|
+
* threading a new parameter.
|
|
564
|
+
*
|
|
565
|
+
* @task T10513
|
|
566
|
+
* @saga T10377
|
|
567
|
+
*/
|
|
568
|
+
export interface LeadRollupConfig {
|
|
569
|
+
/**
|
|
570
|
+
* Roll-up operating mode. Defaults to `'passive'` for backward compatibility.
|
|
571
|
+
*
|
|
572
|
+
* @defaultValue 'passive'
|
|
573
|
+
*/
|
|
574
|
+
mode?: LeadRollupMode;
|
|
575
|
+
}
|
|
576
|
+
|
|
533
577
|
/** CLEO project configuration (config.json). */
|
|
534
578
|
export interface CleoConfig {
|
|
535
579
|
/** Configuration schema version string. */
|
|
@@ -603,6 +647,20 @@ export interface CleoConfig {
|
|
|
603
647
|
* @task T9410
|
|
604
648
|
*/
|
|
605
649
|
auth?: AuthConfig;
|
|
650
|
+
/**
|
|
651
|
+
* Lead-tier wave roll-up configuration.
|
|
652
|
+
*
|
|
653
|
+
* Gates the Lead↔Worker Max-N loop scaffolded under SG-IVTR-AC-BINDING
|
|
654
|
+
* (T10377) and council action #9 of E-VALIDATOR-ROLE (T10383). When
|
|
655
|
+
* absent, the roll-up defaults to `{ mode: 'passive' }` — the legacy
|
|
656
|
+
* compute-from-manifest behaviour — so existing callers continue to work
|
|
657
|
+
* unchanged.
|
|
658
|
+
*
|
|
659
|
+
* @defaultValue undefined
|
|
660
|
+
* @task T10513
|
|
661
|
+
* @saga T10377
|
|
662
|
+
*/
|
|
663
|
+
leadRollup?: LeadRollupConfig;
|
|
606
664
|
}
|
|
607
665
|
|
|
608
666
|
/**
|
package/src/data-accessor.ts
CHANGED
|
@@ -107,6 +107,49 @@ export interface TaskFieldUpdates {
|
|
|
107
107
|
pipelineStage?: string | null;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* A row of the `task_acceptance_criteria` table (T10502).
|
|
112
|
+
*
|
|
113
|
+
* @task T10508
|
|
114
|
+
*/
|
|
115
|
+
export interface AcRow {
|
|
116
|
+
/** UUIDv4 stable identifier, immutable for the AC's lifetime. */
|
|
117
|
+
id: string;
|
|
118
|
+
/** Owning task ID. */
|
|
119
|
+
taskId: string;
|
|
120
|
+
/** 1-based ordinal — never reused per task (gaps remain on shrink). */
|
|
121
|
+
ordinal: number;
|
|
122
|
+
/** The AC statement text. Structured gates are serialised as JSON. */
|
|
123
|
+
text: string;
|
|
124
|
+
/** ISO-8601 timestamp the row was created. */
|
|
125
|
+
createdAt: string;
|
|
126
|
+
/** ISO-8601 last-edit timestamp; null until first edit. */
|
|
127
|
+
updatedAt: string | null;
|
|
128
|
+
/** Optional sha256(text) snapshot; writers MAY populate, readers MUST treat null as "unknown". */
|
|
129
|
+
contentHash: string | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A row of the `evidence_ac_bindings` table (T10503) — the M:N join between
|
|
134
|
+
* evidence atoms and acceptance criteria. Powers the AC-coverage gate
|
|
135
|
+
* (T10509) — "what evidence has been recorded against this AC?".
|
|
136
|
+
*
|
|
137
|
+
* @task T10509
|
|
138
|
+
* @saga T10377 (SG-IVTR-AC-BINDING)
|
|
139
|
+
*/
|
|
140
|
+
export interface AcBindingRow {
|
|
141
|
+
/** UUIDv4 — set by the writer (T10505/T10506). */
|
|
142
|
+
id: string;
|
|
143
|
+
/** Stable hash / composite key of the evidence atom. NOT an FK. */
|
|
144
|
+
evidenceAtomId: string;
|
|
145
|
+
/** FK → `task_acceptance_criteria(id)`. */
|
|
146
|
+
acId: string;
|
|
147
|
+
/** One of {direct, satisfies, coverage}. */
|
|
148
|
+
bindingType: 'direct' | 'satisfies' | 'coverage';
|
|
149
|
+
/** ISO-8601 timestamp of binding creation. */
|
|
150
|
+
createdAt: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
110
153
|
/**
|
|
111
154
|
* Subset of DataAccessor methods available inside a transaction callback.
|
|
112
155
|
* Write-only — reads use the outer accessor (snapshot isolation).
|
|
@@ -129,8 +172,70 @@ export interface TransactionAccessor {
|
|
|
129
172
|
removeRelation(taskId: string, relatedTo: string, relationType?: string): Promise<void>;
|
|
130
173
|
/** Remove all relations for a task (both directions) — used for set-replace. @task T9514 */
|
|
131
174
|
clearRelations(taskId: string): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Insert AC rows into `task_acceptance_criteria` with caller-supplied
|
|
177
|
+
* UUIDs and ordinals. Ordinals MUST NOT collide with existing rows for
|
|
178
|
+
* the same task — the UNIQUE (task_id, ordinal) index enforces this.
|
|
179
|
+
* @task T10508
|
|
180
|
+
*/
|
|
181
|
+
insertAcRows(
|
|
182
|
+
rows: Array<{ id: string; taskId: string; ordinal: number; text: string }>,
|
|
183
|
+
): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Read all AC rows for a task, ordered by ordinal ASC.
|
|
186
|
+
* Available inside transactions for shrink/replace flows that need to
|
|
187
|
+
* read the current state before deletion.
|
|
188
|
+
* @task T10508
|
|
189
|
+
*/
|
|
190
|
+
getAcRows(taskId: string): Promise<AcRow[]>;
|
|
191
|
+
/**
|
|
192
|
+
* Delete all AC rows for a task. Used by update-replace-all + update-shrink
|
|
193
|
+
* flows AFTER the history rows have been appended.
|
|
194
|
+
* @task T10508
|
|
195
|
+
*/
|
|
196
|
+
deleteAcRowsForTask(taskId: string): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Append a history row to `task_acceptance_criteria_history` capturing the
|
|
199
|
+
* AC text that is about to be superseded.
|
|
200
|
+
* @task T10508
|
|
201
|
+
*/
|
|
202
|
+
appendAcHistory(
|
|
203
|
+
rows: Array<{ acId: string; previousText: string; reason: string }>,
|
|
204
|
+
): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Read all `evidence_ac_bindings` rows whose `ac_id` ∈ the given set.
|
|
207
|
+
* Used by the AC-coverage gate (T10509) to compute which ACs are
|
|
208
|
+
* satisfied vs unsatisfied inside the same transaction that flips the
|
|
209
|
+
* task to `done`.
|
|
210
|
+
*
|
|
211
|
+
* Returns the empty array when `acIds` is empty.
|
|
212
|
+
*
|
|
213
|
+
* @task T10509
|
|
214
|
+
*/
|
|
215
|
+
getAcBindings(acIds: readonly string[]): Promise<AcBindingRow[]>;
|
|
216
|
+
/**
|
|
217
|
+
* Insert rows into `evidence_ac_bindings`. Used by the Validator SDK
|
|
218
|
+
* tools (T10511) to persist coverage bindings transactionally after a
|
|
219
|
+
* Validator attestation. The UNIQUE (evidence_atom_id, ac_id, binding_type)
|
|
220
|
+
* index collapses idempotent re-inserts via `ON CONFLICT DO NOTHING`.
|
|
221
|
+
*
|
|
222
|
+
* No-op when `rows` is empty.
|
|
223
|
+
*
|
|
224
|
+
* @task T10511
|
|
225
|
+
* @saga T10377 (SG-IVTR-AC-BINDING)
|
|
226
|
+
*/
|
|
227
|
+
insertAcBindings(
|
|
228
|
+
rows: Array<{
|
|
229
|
+
id: string;
|
|
230
|
+
evidenceAtomId: string;
|
|
231
|
+
acId: string;
|
|
232
|
+
bindingType: 'direct' | 'satisfies' | 'coverage';
|
|
233
|
+
}>,
|
|
234
|
+
): Promise<void>;
|
|
132
235
|
}
|
|
133
236
|
|
|
237
|
+
// Re-export AcRow at the module level for both transaction + outer accessor use.
|
|
238
|
+
|
|
134
239
|
/**
|
|
135
240
|
* DataAccessor interface.
|
|
136
241
|
*
|
|
@@ -193,6 +298,21 @@ export interface DataAccessor {
|
|
|
193
298
|
/** Remove a row from the task_relations table (T9240). */
|
|
194
299
|
removeRelation(taskId: string, relatedTo: string, relationType?: string): Promise<void>;
|
|
195
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Read AC rows for a task from `task_acceptance_criteria`, ordered by
|
|
303
|
+
* ordinal ASC. Returns the empty array if no rows exist.
|
|
304
|
+
* @task T10508
|
|
305
|
+
*/
|
|
306
|
+
getAcRows(taskId: string): Promise<AcRow[]>;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Read `evidence_ac_bindings` rows whose `ac_id` ∈ the given set.
|
|
310
|
+
* Powers the AC-coverage gate (T10509). Returns the empty array when
|
|
311
|
+
* `acIds` is empty or no bindings exist for the supplied ids.
|
|
312
|
+
* @task T10509
|
|
313
|
+
*/
|
|
314
|
+
getAcBindings(acIds: readonly string[]): Promise<AcBindingRow[]>;
|
|
315
|
+
|
|
196
316
|
// ---- Metadata (schema_meta KV store) ----
|
|
197
317
|
|
|
198
318
|
/** Read a typed value from the metadata store. Returns null if not found. */
|