@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.
Files changed (45) hide show
  1. package/dist/__tests__/satisfies-atom.test.d.ts +24 -0
  2. package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
  3. package/dist/__tests__/satisfies-atom.test.js +302 -0
  4. package/dist/__tests__/satisfies-atom.test.js.map +1 -0
  5. package/dist/__tests__/validator-types.test.d.ts +13 -0
  6. package/dist/__tests__/validator-types.test.d.ts.map +1 -0
  7. package/dist/__tests__/validator-types.test.js +293 -0
  8. package/dist/__tests__/validator-types.test.js.map +1 -0
  9. package/dist/config.d.ts +56 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/data-accessor.d.ts +117 -0
  12. package/dist/data-accessor.d.ts.map +1 -1
  13. package/dist/data-accessor.js.map +1 -1
  14. package/dist/evidence-atom-schema.d.ts +91 -0
  15. package/dist/evidence-atom-schema.d.ts.map +1 -1
  16. package/dist/evidence-atom-schema.js +162 -1
  17. package/dist/evidence-atom-schema.js.map +1 -1
  18. package/dist/exit-codes.d.ts +41 -1
  19. package/dist/exit-codes.d.ts.map +1 -1
  20. package/dist/exit-codes.js +40 -0
  21. package/dist/exit-codes.js.map +1 -1
  22. package/dist/index.d.ts +7 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/operations/tasks.d.ts +49 -0
  27. package/dist/operations/tasks.d.ts.map +1 -1
  28. package/dist/operations/tasks.js.map +1 -1
  29. package/dist/task.d.ts +38 -0
  30. package/dist/task.d.ts.map +1 -1
  31. package/dist/validator/index.d.ts +375 -0
  32. package/dist/validator/index.d.ts.map +1 -0
  33. package/dist/validator/index.js +162 -0
  34. package/dist/validator/index.js.map +1 -0
  35. package/package.json +2 -2
  36. package/src/__tests__/satisfies-atom.test.ts +363 -0
  37. package/src/__tests__/validator-types.test.ts +359 -0
  38. package/src/config.ts +58 -0
  39. package/src/data-accessor.ts +120 -0
  40. package/src/evidence-atom-schema.ts +188 -1
  41. package/src/exit-codes.ts +42 -0
  42. package/src/index.ts +37 -0
  43. package/src/operations/tasks.ts +50 -0
  44. package/src/task.ts +39 -0
  45. 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
  /**
@@ -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. */