@cleocode/contracts 2026.4.69 → 2026.4.72

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 (71) hide show
  1. package/dist/__tests__/acceptance-gate.test.d.ts +14 -0
  2. package/dist/__tests__/acceptance-gate.test.d.ts.map +1 -0
  3. package/dist/__tests__/acceptance-gate.test.js +642 -0
  4. package/dist/__tests__/acceptance-gate.test.js.map +1 -0
  5. package/dist/__tests__/attachment.test.d.ts +17 -0
  6. package/dist/__tests__/attachment.test.d.ts.map +1 -0
  7. package/dist/__tests__/attachment.test.js +505 -0
  8. package/dist/__tests__/attachment.test.js.map +1 -0
  9. package/dist/__tests__/evidence-record.test.d.ts +14 -0
  10. package/dist/__tests__/evidence-record.test.d.ts.map +1 -0
  11. package/dist/__tests__/evidence-record.test.js +362 -0
  12. package/dist/__tests__/evidence-record.test.js.map +1 -0
  13. package/dist/__tests__/invariants.test.d.ts +19 -0
  14. package/dist/__tests__/invariants.test.d.ts.map +1 -0
  15. package/dist/__tests__/invariants.test.js +351 -0
  16. package/dist/__tests__/invariants.test.js.map +1 -0
  17. package/dist/acceptance-gate-schema.d.ts +701 -0
  18. package/dist/acceptance-gate-schema.d.ts.map +1 -0
  19. package/dist/acceptance-gate-schema.js +303 -0
  20. package/dist/acceptance-gate-schema.js.map +1 -0
  21. package/dist/acceptance-gate.d.ts +350 -0
  22. package/dist/acceptance-gate.d.ts.map +1 -0
  23. package/dist/acceptance-gate.js +22 -0
  24. package/dist/acceptance-gate.js.map +1 -0
  25. package/dist/attachment-schema.d.ts +243 -0
  26. package/dist/attachment-schema.d.ts.map +1 -0
  27. package/dist/attachment-schema.js +200 -0
  28. package/dist/attachment-schema.js.map +1 -0
  29. package/dist/attachment.d.ts +224 -0
  30. package/dist/attachment.d.ts.map +1 -0
  31. package/dist/attachment.js +21 -0
  32. package/dist/attachment.js.map +1 -0
  33. package/dist/evidence-record-schema.d.ts +202 -0
  34. package/dist/evidence-record-schema.d.ts.map +1 -0
  35. package/dist/evidence-record-schema.js +130 -0
  36. package/dist/evidence-record-schema.js.map +1 -0
  37. package/dist/evidence-record.d.ts +167 -0
  38. package/dist/evidence-record.d.ts.map +1 -0
  39. package/dist/evidence-record.js +21 -0
  40. package/dist/evidence-record.js.map +1 -0
  41. package/dist/index.d.ts +12 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/task-evidence.d.ts +251 -0
  46. package/dist/task-evidence.d.ts.map +1 -0
  47. package/dist/task-evidence.js +89 -0
  48. package/dist/task-evidence.js.map +1 -0
  49. package/dist/task.d.ts +23 -4
  50. package/dist/task.d.ts.map +1 -1
  51. package/package.json +8 -2
  52. package/schemas/acceptance-gate.schema.json +9 -0
  53. package/schemas/attachment.schema.json +9 -0
  54. package/schemas/gate-result-details.schema.json +9 -0
  55. package/schemas/gate-result.schema.json +9 -0
  56. package/schemas/manifest-v1.json +176 -0
  57. package/schemas/task-evidence.schema.json +9 -0
  58. package/schemas/task.schema.json +9 -0
  59. package/src/__tests__/acceptance-gate.test.ts +736 -0
  60. package/src/__tests__/attachment.test.ts +601 -0
  61. package/src/__tests__/evidence-record.test.ts +412 -0
  62. package/src/__tests__/invariants.test.ts +445 -0
  63. package/src/acceptance-gate-schema.ts +347 -0
  64. package/src/acceptance-gate.ts +370 -0
  65. package/src/attachment-schema.ts +223 -0
  66. package/src/attachment.ts +245 -0
  67. package/src/evidence-record-schema.ts +158 -0
  68. package/src/evidence-record.ts +182 -0
  69. package/src/index.ts +114 -1
  70. package/src/task-evidence.ts +239 -0
  71. package/src/task.ts +24 -4
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Zod validation schemas for {@link AcceptanceGate} and related types.
3
+ *
4
+ * Kept in a separate file from the plain TypeScript types so that consumers
5
+ * that do not need runtime validation (e.g. type-only imports) pay zero cost.
6
+ *
7
+ * All schemas are named `*Schema` and derive their inferred types from the
8
+ * canonical TypeScript interfaces in `./acceptance-gate.ts`.
9
+ *
10
+ * @epic T760
11
+ * @task T763
12
+ * @task T779
13
+ */
14
+
15
+ import { z } from 'zod';
16
+
17
+ // ─── Base schema ──────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Zod schema for {@link GateBase} — fields every gate variant carries.
21
+ *
22
+ * Not exported as a standalone validator because it is always composed into a
23
+ * full gate schema via `z.discriminatedUnion`. Exported so downstream packages
24
+ * can extend it (e.g. for migration or testing).
25
+ */
26
+ export const gateBaseSchema = z.object({
27
+ /** Optional GSD-style REQ-ID, e.g. `"TIMER-03"`. */
28
+ req: z.string().optional(),
29
+ /** Human-readable description of what the gate checks. Required. */
30
+ description: z.string().min(1),
31
+ /** When true, failure is recorded as a warning rather than a block. */
32
+ advisory: z.boolean().optional(),
33
+ /** Per-gate timeout override in milliseconds. */
34
+ timeoutMs: z.number().int().positive().optional(),
35
+ });
36
+
37
+ // ─── File assertion schemas ───────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Zod schema for {@link FileAssertion}.
41
+ *
42
+ * Each variant carries a `type` literal that drives the discriminated union.
43
+ */
44
+ export const fileAssertionSchema = z.discriminatedUnion('type', [
45
+ z.object({ type: z.literal('exists') }),
46
+ z.object({ type: z.literal('absent') }),
47
+ z.object({ type: z.literal('nonEmpty') }),
48
+ z.object({ type: z.literal('maxBytes'), value: z.number().int().nonnegative() }),
49
+ z.object({ type: z.literal('minBytes'), value: z.number().int().nonnegative() }),
50
+ z.object({ type: z.literal('contains'), value: z.string() }),
51
+ z.object({
52
+ type: z.literal('matches'),
53
+ /** Serialized `RegExp` source string. */
54
+ regex: z.string().min(1),
55
+ /** Optional `RegExp` flags, e.g. `"gim"`. */
56
+ flags: z.string().optional(),
57
+ }),
58
+ z.object({ type: z.literal('sha256'), value: z.string().length(64) }),
59
+ ]);
60
+
61
+ /** Inferred TypeScript type from {@link fileAssertionSchema}. */
62
+ export type FileAssertionInput = z.input<typeof fileAssertionSchema>;
63
+
64
+ // ─── Individual gate schemas ──────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Zod schema for {@link TestGate}.
68
+ *
69
+ * Validates that `command` is present and `expect` is one of the two allowed
70
+ * literal values.
71
+ */
72
+ export const testGateSchema = gateBaseSchema.extend({
73
+ kind: z.literal('test'),
74
+ command: z.string().min(1),
75
+ args: z.array(z.string()).optional(),
76
+ expect: z.enum(['pass', 'exit0']),
77
+ minCount: z.number().int().nonnegative().optional(),
78
+ cwd: z.string().optional(),
79
+ env: z.record(z.string(), z.string()).optional(),
80
+ });
81
+
82
+ /** Zod schema for {@link FileGate}. */
83
+ export const fileGateSchema = gateBaseSchema.extend({
84
+ kind: z.literal('file'),
85
+ /** Absolute or project-root-relative file path. Mutually exclusive with `attachmentSha256`. */
86
+ path: z.string().min(1).optional(),
87
+ /**
88
+ * SHA-256 hex of an attachment blob. Gate runner resolves the on-disk path
89
+ * via AttachmentStore. Mutually exclusive with `path`.
90
+ */
91
+ attachmentSha256: z
92
+ .string()
93
+ .regex(/^[0-9a-f]{64}$/i, 'attachmentSha256 must be a 64-char hex string')
94
+ .optional(),
95
+ assertions: z.array(fileAssertionSchema).min(1),
96
+ });
97
+
98
+ /** Zod schema for {@link CommandGate}. */
99
+ export const commandGateSchema = gateBaseSchema.extend({
100
+ kind: z.literal('command'),
101
+ cmd: z.string().min(1),
102
+ args: z.array(z.string()).optional(),
103
+ exitCode: z.number().int().optional(),
104
+ stdoutMatches: z.string().optional(),
105
+ stderrMatches: z.string().optional(),
106
+ cwd: z.string().optional(),
107
+ env: z.record(z.string(), z.string()).optional(),
108
+ });
109
+
110
+ /** Zod schema for {@link LintGate}. */
111
+ export const lintGateSchema = gateBaseSchema.extend({
112
+ kind: z.literal('lint'),
113
+ tool: z.enum(['biome', 'eslint', 'tsc', 'prettier', 'rustc', 'clippy']),
114
+ args: z.array(z.string()).optional(),
115
+ expect: z.enum(['clean', 'noErrors']),
116
+ cwd: z.string().optional(),
117
+ });
118
+
119
+ /** Zod schema for {@link HttpGate}. */
120
+ export const httpGateSchema = gateBaseSchema.extend({
121
+ kind: z.literal('http'),
122
+ url: z.string().url(),
123
+ method: z.enum(['GET', 'HEAD', 'POST', 'PUT', 'DELETE']).optional(),
124
+ status: z.number().int().min(100).max(599),
125
+ bodyMatches: z.string().optional(),
126
+ headers: z.record(z.string(), z.string()).optional(),
127
+ startCommand: z.string().optional(),
128
+ startupDelayMs: z.number().int().nonnegative().optional(),
129
+ });
130
+
131
+ /** Zod schema for {@link ManualGate}. */
132
+ export const manualGateSchema = gateBaseSchema.extend({
133
+ kind: z.literal('manual'),
134
+ prompt: z.string().min(1),
135
+ verdicts: z.array(z.enum(['pass', 'fail', 'warn'])).optional(),
136
+ });
137
+
138
+ // ─── Discriminated union ──────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Zod discriminated-union schema for {@link AcceptanceGate}.
142
+ *
143
+ * Uses `z.discriminatedUnion('kind', [...])` for O(1) variant lookup.
144
+ * Parse with `acceptanceGateSchema.parse(input)` or
145
+ * `acceptanceGateSchema.safeParse(input)`.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const gate = acceptanceGateSchema.parse({
150
+ * kind: 'test',
151
+ * command: 'pnpm test',
152
+ * expect: 'pass',
153
+ * description: 'Test suite must pass',
154
+ * });
155
+ * ```
156
+ */
157
+ export const acceptanceGateSchema = z.discriminatedUnion('kind', [
158
+ testGateSchema,
159
+ fileGateSchema,
160
+ commandGateSchema,
161
+ lintGateSchema,
162
+ httpGateSchema,
163
+ manualGateSchema,
164
+ ]);
165
+
166
+ /** Inferred TypeScript type from {@link acceptanceGateSchema}. */
167
+ export type AcceptanceGateSchemaInput = z.input<typeof acceptanceGateSchema>;
168
+
169
+ // ─── GateResultDetails schema (T802) ─────────────────────────────────────────
170
+
171
+ /**
172
+ * Zod discriminated-union schema for {@link GateResultDetails}.
173
+ *
174
+ * Each variant mirrors the corresponding {@link AcceptanceGate} kind so
175
+ * callers can narrow `details` using the `kind` discriminant.
176
+ *
177
+ * @epic T760
178
+ * @task T802
179
+ */
180
+ export const gateResultDetailsSchema = z.discriminatedUnion('kind', [
181
+ z.object({
182
+ kind: z.literal('test'),
183
+ exitCode: z.number().int(),
184
+ stdout: z.string(),
185
+ stderr: z.string(),
186
+ duration: z.number().nonnegative(),
187
+ }),
188
+ z.object({
189
+ kind: z.literal('file'),
190
+ path: z.string().min(1),
191
+ passedAssertions: z.array(z.string()),
192
+ failedAssertions: z.array(z.string()),
193
+ }),
194
+ z.object({
195
+ kind: z.literal('command'),
196
+ cmd: z.string().min(1),
197
+ exitCode: z.number().int(),
198
+ stdout: z.string(),
199
+ }),
200
+ z.object({
201
+ kind: z.literal('lint'),
202
+ tool: z.string().min(1),
203
+ warnings: z.number().int().nonnegative(),
204
+ errors: z.number().int().nonnegative(),
205
+ }),
206
+ z.object({
207
+ kind: z.literal('http'),
208
+ url: z.string(),
209
+ status: z.number().int().min(100).max(599),
210
+ body: z.string(),
211
+ }),
212
+ z.object({
213
+ kind: z.literal('manual'),
214
+ prompt: z.string().min(1),
215
+ accepted: z.boolean(),
216
+ }),
217
+ ]);
218
+
219
+ /** Inferred TypeScript type from {@link gateResultDetailsSchema}. */
220
+ export type GateResultDetailsInput = z.input<typeof gateResultDetailsSchema>;
221
+
222
+ // ─── Result schema ────────────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Zod schema for {@link AcceptanceGateResult}.
226
+ *
227
+ * Validates the shape produced by the gate runner and stored in
228
+ * `lifecycle_gate_results`.
229
+ */
230
+ export const acceptanceGateResultSchema = z.object({
231
+ index: z.number().int().nonnegative(),
232
+ req: z.string().optional(),
233
+ kind: z.enum(['test', 'file', 'command', 'lint', 'http', 'manual']),
234
+ result: z.enum(['pass', 'fail', 'warn', 'skipped', 'error']),
235
+ durationMs: z.number().nonnegative(),
236
+ /** Typed kind-specific detail payload (T802). */
237
+ details: gateResultDetailsSchema.optional(),
238
+ evidence: z.string().optional(),
239
+ errorMessage: z.string().optional(),
240
+ /** ISO 8601 timestamp. */
241
+ checkedAt: z.string().datetime(),
242
+ checkedBy: z.string().min(1),
243
+ });
244
+
245
+ /** Inferred TypeScript type from {@link acceptanceGateResultSchema}. */
246
+ export type AcceptanceGateResultInput = z.input<typeof acceptanceGateResultSchema>;
247
+
248
+ // ─── Mixed acceptance item schema (T780 + T800) ───────────────────────────────
249
+
250
+ /**
251
+ * Zod schema for a single acceptance criterion — either a non-empty string
252
+ * (legacy) or a structured {@link AcceptanceGate} (machine-verifiable).
253
+ *
254
+ * **Tightened (T800)**: Rejects empty strings, whitespace-only strings, and
255
+ * any non-gate objects. Clear error messages guide callers toward valid input.
256
+ *
257
+ * Strings are validated with `z.string().trim().min(1)` to eliminate empty
258
+ * and whitespace-only criteria. The gate branch uses the existing discriminated-union
259
+ * schema so all six gate kinds (`test`, `file`, `command`, `lint`, `http`, `manual`)
260
+ * are accepted.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * acceptanceItemSchema.parse('must render in <100ms'); // => 'must render in <100ms'
265
+ * acceptanceItemSchema.parse({ kind: 'test', command: 'pnpm test', expect: 'pass', description: '...' }); // => TestGate
266
+ * acceptanceItemSchema.parse(''); // ERROR: string must be non-empty
267
+ * acceptanceItemSchema.parse({ foo: 'bar' }); // ERROR: invalid gate object
268
+ * ```
269
+ *
270
+ * @epic T760
271
+ * @task T780
272
+ * @task T800
273
+ */
274
+ export const acceptanceItemSchema = z.union([
275
+ z
276
+ .string()
277
+ .trim()
278
+ .min(1, {
279
+ message:
280
+ 'Acceptance string criterion must be non-empty. Provide a non-blank description or a valid AcceptanceGate object.',
281
+ })
282
+ .describe('Free-text legacy acceptance criterion (non-empty string)'),
283
+ acceptanceGateSchema.describe('Structured machine-verifiable acceptance gate'),
284
+ ]);
285
+
286
+ /** Inferred TypeScript type from {@link acceptanceItemSchema}. */
287
+ export type AcceptanceItemInput = z.input<typeof acceptanceItemSchema>;
288
+
289
+ /**
290
+ * Zod schema for a mixed acceptance array — `(string | AcceptanceGate)[]`.
291
+ *
292
+ * **Tightened (T800)**:
293
+ * - Requires at least 1 item (matching CLEO's anti-hallucination policy).
294
+ * - Rejects duplicate `req:` GSD-style REQ-IDs within the same array.
295
+ *
296
+ * Use this to validate the full `Task.acceptance` field after widening (T780).
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * // Valid: mixed types with unique req IDs
301
+ * acceptanceArraySchema.parse([
302
+ * 'must render in <100ms',
303
+ * { kind: 'test', command: 'pnpm test', expect: 'pass', description: 'Tests pass', req: 'TEST-01' }
304
+ * ]);
305
+ *
306
+ * // ERROR: empty array
307
+ * acceptanceArraySchema.parse([]);
308
+ *
309
+ * // ERROR: duplicate req:'TEST-01' across two gates
310
+ * acceptanceArraySchema.parse([
311
+ * { kind: 'test', command: 'pnpm test', expect: 'pass', description: 'Tests pass', req: 'TEST-01' },
312
+ * { kind: 'file', path: 'README.md', assertions: [{ type: 'exists' }], description: 'README exists', req: 'TEST-01' }
313
+ * ]);
314
+ * ```
315
+ *
316
+ * @epic T760
317
+ * @task T780
318
+ * @task T800
319
+ */
320
+ export const acceptanceArraySchema = z
321
+ .array(acceptanceItemSchema)
322
+ .min(1, {
323
+ message:
324
+ 'Task must have at least one acceptance criterion. Provide a non-empty string or a valid AcceptanceGate object.',
325
+ })
326
+ .refine(
327
+ (items) => {
328
+ // Extract all req: values from gates (skip plain strings)
329
+ const reqIds = new Set<string>();
330
+ for (const item of items) {
331
+ if (typeof item === 'object' && item !== null && 'req' in item && item.req) {
332
+ if (reqIds.has(item.req)) {
333
+ return false; // Duplicate found
334
+ }
335
+ reqIds.add(item.req);
336
+ }
337
+ }
338
+ return true; // No duplicates
339
+ },
340
+ {
341
+ message:
342
+ 'Duplicate req: (REQ-ID) values within the same acceptance array. Each GSD-style requirement ID must be unique. Check for repeated req fields and ensure each gate has a distinct identifier.',
343
+ },
344
+ );
345
+
346
+ /** Inferred TypeScript type from {@link acceptanceArraySchema}. */
347
+ export type AcceptanceArrayInput = z.input<typeof acceptanceArraySchema>;