@cleocode/contracts 2026.5.120 → 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.
Files changed (63) hide show
  1. package/dist/__tests__/changesets.test.d.ts +8 -0
  2. package/dist/__tests__/changesets.test.d.ts.map +1 -0
  3. package/dist/__tests__/changesets.test.js +146 -0
  4. package/dist/__tests__/changesets.test.js.map +1 -0
  5. package/dist/__tests__/evidence-atom-schema.test.js +75 -1
  6. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  7. package/dist/__tests__/satisfies-atom.test.d.ts +24 -0
  8. package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
  9. package/dist/__tests__/satisfies-atom.test.js +302 -0
  10. package/dist/__tests__/satisfies-atom.test.js.map +1 -0
  11. package/dist/__tests__/validator-types.test.d.ts +13 -0
  12. package/dist/__tests__/validator-types.test.d.ts.map +1 -0
  13. package/dist/__tests__/validator-types.test.js +293 -0
  14. package/dist/__tests__/validator-types.test.js.map +1 -0
  15. package/dist/changesets.d.ts +91 -0
  16. package/dist/changesets.d.ts.map +1 -1
  17. package/dist/changesets.js +72 -2
  18. package/dist/changesets.js.map +1 -1
  19. package/dist/config.d.ts +56 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/data-accessor.d.ts +117 -0
  22. package/dist/data-accessor.d.ts.map +1 -1
  23. package/dist/data-accessor.js.map +1 -1
  24. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  25. package/dist/dispatch/operations-registry.js +29 -0
  26. package/dist/dispatch/operations-registry.js.map +1 -1
  27. package/dist/evidence-atom-schema.d.ts +143 -8
  28. package/dist/evidence-atom-schema.d.ts.map +1 -1
  29. package/dist/evidence-atom-schema.js +258 -1
  30. package/dist/evidence-atom-schema.js.map +1 -1
  31. package/dist/exit-codes.d.ts +41 -1
  32. package/dist/exit-codes.d.ts.map +1 -1
  33. package/dist/exit-codes.js +40 -0
  34. package/dist/exit-codes.js.map +1 -1
  35. package/dist/index.d.ts +8 -6
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/operations/tasks.d.ts +75 -0
  40. package/dist/operations/tasks.d.ts.map +1 -1
  41. package/dist/operations/tasks.js.map +1 -1
  42. package/dist/task.d.ts +38 -0
  43. package/dist/task.d.ts.map +1 -1
  44. package/dist/validator/index.d.ts +375 -0
  45. package/dist/validator/index.d.ts.map +1 -0
  46. package/dist/validator/index.js +162 -0
  47. package/dist/validator/index.js.map +1 -0
  48. package/package.json +2 -2
  49. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  50. package/src/__tests__/changesets.test.ts +176 -0
  51. package/src/__tests__/evidence-atom-schema.test.ts +88 -0
  52. package/src/__tests__/satisfies-atom.test.ts +363 -0
  53. package/src/__tests__/validator-types.test.ts +359 -0
  54. package/src/changesets.ts +93 -2
  55. package/src/config.ts +58 -0
  56. package/src/data-accessor.ts +120 -0
  57. package/src/dispatch/operations-registry.ts +31 -0
  58. package/src/evidence-atom-schema.ts +319 -8
  59. package/src/exit-codes.ts +42 -0
  60. package/src/index.ts +39 -0
  61. package/src/operations/tasks.ts +76 -0
  62. package/src/task.ts +39 -0
  63. 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/changesets.ts CHANGED
@@ -67,6 +67,91 @@ export const CHANGESET_KINDS = [
67
67
  /** Allowed values of {@link ChangesetEntry.kind}. */
68
68
  export type ChangesetKind = (typeof CHANGESET_KINDS)[number];
69
69
 
70
+ // ─── Release-note metadata ───────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Deterministic release-note section override used by zero-token renderers.
74
+ *
75
+ * These values intentionally mirror Keep-a-Changelog style headings so release
76
+ * tooling can render higher-quality notes without an LLM/API/token dependency.
77
+ */
78
+ export const CHANGESET_RELEASE_NOTE_SECTIONS = [
79
+ 'added',
80
+ 'changed',
81
+ 'fixed',
82
+ 'deprecated',
83
+ 'removed',
84
+ 'security',
85
+ 'breaking',
86
+ ] as const;
87
+
88
+ /** Allowed values of {@link ChangesetReleaseNotesMetadata.section}. */
89
+ export type ChangesetReleaseNoteSection = (typeof CHANGESET_RELEASE_NOTE_SECTIONS)[number];
90
+
91
+ /** Audience labels for deterministic release-note routing. */
92
+ export const CHANGESET_RELEASE_NOTE_AUDIENCES = [
93
+ 'users',
94
+ 'operators',
95
+ 'developers',
96
+ 'maintainers',
97
+ ] as const;
98
+
99
+ /** Allowed values of {@link ChangesetReleaseNotesMetadata.audience}. */
100
+ export type ChangesetReleaseNoteAudience = (typeof CHANGESET_RELEASE_NOTE_AUDIENCES)[number];
101
+
102
+ /** Scope labels for deterministic release-note routing. */
103
+ export const CHANGESET_RELEASE_NOTE_SCOPES = [
104
+ 'project',
105
+ 'package',
106
+ 'component',
107
+ 'docs',
108
+ 'ops',
109
+ 'security',
110
+ ] as const;
111
+
112
+ /** Allowed values of {@link ChangesetReleaseNotesMetadata.scope}. */
113
+ export type ChangesetReleaseNoteScope = (typeof CHANGESET_RELEASE_NOTE_SCOPES)[number];
114
+
115
+ const nonEmptyReleaseNoteText = z.string().min(1, 'release-note metadata text must be non-empty');
116
+ const nonEmptyReleaseNoteList = z
117
+ .array(nonEmptyReleaseNoteText)
118
+ .min(1, 'release-note metadata list must contain at least one item');
119
+
120
+ /**
121
+ * Optional deterministic metadata for release-note rendering.
122
+ *
123
+ * This is deliberately small, explicit, and strict: it gives release tooling
124
+ * structured author-provided facts (section, audience, target, impact, etc.)
125
+ * while preserving zero-token defaults and failing loudly on misspelled fields.
126
+ */
127
+ export const ChangesetReleaseNotesMetadataSchema = z
128
+ .object({
129
+ /** Keep-a-Changelog style section/category override. */
130
+ section: z.enum(CHANGESET_RELEASE_NOTE_SECTIONS).optional(),
131
+ /** Intended readers for the note. */
132
+ audience: z.array(z.enum(CHANGESET_RELEASE_NOTE_AUDIENCES)).min(1).optional(),
133
+ /** Release-note scope. */
134
+ scope: z.enum(CHANGESET_RELEASE_NOTE_SCOPES).optional(),
135
+ /** Package/project/component targets affected by this entry. */
136
+ targets: nonEmptyReleaseNoteList.optional(),
137
+ /** User- or operator-visible impact statement. */
138
+ impact: nonEmptyReleaseNoteText.optional(),
139
+ /** Deterministic migration/action text, separate from breaking changes. */
140
+ migration: nonEmptyReleaseNoteText.optional(),
141
+ /** Deprecation detail for Deprecated sections. */
142
+ deprecation: nonEmptyReleaseNoteText.optional(),
143
+ /** Security detail for Security sections. */
144
+ security: nonEmptyReleaseNoteText.optional(),
145
+ /** Operator-facing rollout/verification note. */
146
+ operatorNotes: nonEmptyReleaseNoteText.optional(),
147
+ /** Explicit inclusion toggle for future release-scope filters. */
148
+ includeInChangelog: z.boolean().optional(),
149
+ })
150
+ .strict();
151
+
152
+ /** Inferred TypeScript type for deterministic release-note metadata. */
153
+ export type ChangesetReleaseNotesMetadata = z.infer<typeof ChangesetReleaseNotesMetadataSchema>;
154
+
70
155
  // ─── Task ID validator ────────────────────────────────────────────────────────
71
156
 
72
157
  /**
@@ -93,14 +178,18 @@ const TASK_ID_RE = /^(T\d+(-[A-Z][A-Za-z0-9]*)?|E-\d+(-[A-Z][A-Za-z0-9]*)?)$/;
93
178
  * - `prs` — linked PR numbers
94
179
  * - `notes` — longer-form markdown body
95
180
  * - `breaking` — required migration note when `kind === 'breaking'`
181
+ * - `releaseNotes` — deterministic zero-token release-note metadata
96
182
  */
97
183
  export const ChangesetEntrySchema = z
98
184
  .object({
99
- /** Filename slug (without the `.md` extension). */
185
+ /** Filename identifier (without the `.md` extension); lowercase kebab-case for new files, legacy uppercase task IDs accepted. */
100
186
  id: z
101
187
  .string()
102
188
  .min(1, 'id must be non-empty')
103
- .regex(/^[a-z0-9][a-z0-9-]*$/, 'id must be kebab-case (lowercase, digits, hyphens)'),
189
+ .regex(
190
+ /^[A-Za-z0-9][A-Za-z0-9-]*$/,
191
+ 'id must contain only ASCII letters, digits, and hyphens',
192
+ ),
104
193
  /** One or more CLEO task IDs this change is anchored to. */
105
194
  tasks: z
106
195
  .array(z.string().regex(TASK_ID_RE, 'task ID must match T#### or E-#### format'))
@@ -115,6 +204,8 @@ export const ChangesetEntrySchema = z
115
204
  notes: z.string().optional(),
116
205
  /** Migration note. Required iff `kind === 'breaking'`. */
117
206
  breaking: z.string().optional(),
207
+ /** Structured author-provided metadata for deterministic release notes. */
208
+ releaseNotes: ChangesetReleaseNotesMetadataSchema.optional(),
118
209
  })
119
210
  .refine((entry) => entry.kind !== 'breaking' || (entry.breaking?.length ?? 0) > 0, {
120
211
  message: 'breaking entries must include a non-empty `breaking` migration note',
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
  /**
@@ -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. */