@cleocode/contracts 2026.5.112 → 2026.5.114

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 (108) hide show
  1. package/dist/__tests__/cli-meta-suggested-next.test.d.ts +21 -0
  2. package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
  4. package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
  5. package/dist/config/__tests__/manifest.test.d.ts +13 -0
  6. package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
  7. package/dist/config/__tests__/manifest.test.js +143 -0
  8. package/dist/config/__tests__/manifest.test.js.map +1 -0
  9. package/dist/config/manifest.d.ts +158 -0
  10. package/dist/config/manifest.d.ts.map +1 -0
  11. package/dist/config/manifest.js +123 -0
  12. package/dist/config/manifest.js.map +1 -0
  13. package/dist/docs/__tests__/provenance.test.d.ts +14 -0
  14. package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
  15. package/dist/docs/__tests__/provenance.test.js +579 -0
  16. package/dist/docs/__tests__/provenance.test.js.map +1 -0
  17. package/dist/docs/provenance.d.ts +731 -0
  18. package/dist/docs/provenance.d.ts.map +1 -0
  19. package/dist/docs/provenance.js +258 -0
  20. package/dist/docs/provenance.js.map +1 -0
  21. package/dist/docs-taxonomy.d.ts +24 -0
  22. package/dist/docs-taxonomy.d.ts.map +1 -1
  23. package/dist/docs-taxonomy.js +27 -0
  24. package/dist/docs-taxonomy.js.map +1 -1
  25. package/dist/doctor.d.ts +101 -0
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/errors.d.ts +4 -0
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js +4 -0
  30. package/dist/errors.js.map +1 -1
  31. package/dist/exit-codes.d.ts +37 -0
  32. package/dist/exit-codes.d.ts.map +1 -1
  33. package/dist/exit-codes.js +37 -0
  34. package/dist/exit-codes.js.map +1 -1
  35. package/dist/index.d.ts +12 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +11 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
  40. package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
  41. package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
  42. package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
  43. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
  44. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
  45. package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
  46. package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
  47. package/dist/invariants/adr-056-release.d.ts +42 -0
  48. package/dist/invariants/adr-056-release.d.ts.map +1 -0
  49. package/dist/invariants/adr-056-release.js +143 -0
  50. package/dist/invariants/adr-056-release.js.map +1 -0
  51. package/dist/invariants/adr-070-orchestration.d.ts +73 -0
  52. package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
  53. package/dist/invariants/adr-070-orchestration.js +290 -0
  54. package/dist/invariants/adr-070-orchestration.js.map +1 -0
  55. package/dist/invariants/index.d.ts +4 -1
  56. package/dist/invariants/index.d.ts.map +1 -1
  57. package/dist/invariants/index.js +11 -2
  58. package/dist/invariants/index.js.map +1 -1
  59. package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
  60. package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
  61. package/dist/operations/__tests__/input-contract.test.js +158 -0
  62. package/dist/operations/__tests__/input-contract.test.js.map +1 -0
  63. package/dist/operations/__tests__/tasks-input-contracts.test.d.ts +18 -0
  64. package/dist/operations/__tests__/tasks-input-contracts.test.d.ts.map +1 -0
  65. package/dist/operations/__tests__/tasks-input-contracts.test.js +80 -0
  66. package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -0
  67. package/dist/operations/docs.d.ts +16 -0
  68. package/dist/operations/docs.d.ts.map +1 -1
  69. package/dist/operations/input-contract.d.ts +267 -0
  70. package/dist/operations/input-contract.d.ts.map +1 -0
  71. package/dist/operations/input-contract.js +29 -0
  72. package/dist/operations/input-contract.js.map +1 -0
  73. package/dist/operations/tasks.d.ts +96 -0
  74. package/dist/operations/tasks.d.ts.map +1 -1
  75. package/dist/operations/tasks.js +237 -1
  76. package/dist/operations/tasks.js.map +1 -1
  77. package/dist/templates/__tests__/manifest.test.d.ts +18 -0
  78. package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
  79. package/dist/templates/__tests__/manifest.test.js +123 -0
  80. package/dist/templates/__tests__/manifest.test.js.map +1 -0
  81. package/dist/templates/manifest.d.ts +186 -0
  82. package/dist/templates/manifest.d.ts.map +1 -0
  83. package/dist/templates/manifest.js +165 -0
  84. package/dist/templates/manifest.js.map +1 -0
  85. package/package.json +2 -2
  86. package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
  87. package/src/config/__tests__/manifest.test.ts +176 -0
  88. package/src/config/manifest.ts +218 -0
  89. package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
  90. package/src/docs/__tests__/provenance.test.ts +648 -0
  91. package/src/docs/provenance.ts +601 -0
  92. package/src/docs-taxonomy.ts +55 -0
  93. package/src/doctor.ts +111 -0
  94. package/src/errors.ts +4 -0
  95. package/src/exit-codes.ts +39 -0
  96. package/src/index.ts +102 -0
  97. package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
  98. package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
  99. package/src/invariants/adr-056-release.ts +154 -0
  100. package/src/invariants/adr-070-orchestration.ts +317 -0
  101. package/src/invariants/index.ts +11 -2
  102. package/src/operations/__tests__/input-contract.test.ts +188 -0
  103. package/src/operations/__tests__/tasks-input-contracts.test.ts +106 -0
  104. package/src/operations/docs.ts +16 -0
  105. package/src/operations/input-contract.ts +301 -0
  106. package/src/operations/tasks.ts +308 -0
  107. package/src/templates/__tests__/manifest.test.ts +140 -0
  108. package/src/templates/manifest.ts +208 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * Unit + snapshot tests for the Docs Provenance Graph contract (T10166).
3
+ *
4
+ * Coverage:
5
+ * - Round-trip Zod parse for each node variant + edge variant + envelope.
6
+ * - Discriminator rejection for unknown `kind` / `relation` / lifecycle values.
7
+ * - Type-guard narrowing for all five node variants.
8
+ * - Snapshot test for a typical-sized graph (5 docs, 8 tasks, 4 decisions,
9
+ * 2 sessions) — locks the wire shape consumed by the renderer.
10
+ *
11
+ * @task T10166
12
+ */
13
+
14
+ import { describe, expect, it } from 'vitest';
15
+ import {
16
+ type DocProvenanceResponse,
17
+ docLifecycleStatusSchema,
18
+ docProvenanceResponseSchema,
19
+ isDocProvenanceResponse,
20
+ isProvenanceDecisionNode,
21
+ isProvenanceDocNode,
22
+ isProvenanceMemoryNode,
23
+ isProvenanceSessionNode,
24
+ isProvenanceTaskNode,
25
+ type ProvenanceDecisionNode,
26
+ type ProvenanceDocNode,
27
+ type ProvenanceEdge,
28
+ type ProvenanceMemoryNode,
29
+ type ProvenanceNode,
30
+ type ProvenanceSessionNode,
31
+ type ProvenanceTaskNode,
32
+ provenanceDocNodeSchema,
33
+ provenanceEdgeRelationSchema,
34
+ provenanceEdgeSchema,
35
+ provenanceNodeKindSchema,
36
+ provenanceNodeSchema,
37
+ } from '../provenance.js';
38
+
39
+ // ─── Enum schemas ─────────────────────────────────────────────────────────────
40
+
41
+ describe('provenanceNodeKindSchema', () => {
42
+ it('accepts every canonical node kind', () => {
43
+ expect(provenanceNodeKindSchema.parse('doc')).toBe('doc');
44
+ expect(provenanceNodeKindSchema.parse('task')).toBe('task');
45
+ expect(provenanceNodeKindSchema.parse('decision')).toBe('decision');
46
+ expect(provenanceNodeKindSchema.parse('session')).toBe('session');
47
+ expect(provenanceNodeKindSchema.parse('memory')).toBe('memory');
48
+ });
49
+
50
+ it('rejects an unknown node kind', () => {
51
+ expect(() => provenanceNodeKindSchema.parse('artifact')).toThrow();
52
+ });
53
+ });
54
+
55
+ describe('provenanceEdgeRelationSchema', () => {
56
+ it('accepts every canonical edge relation', () => {
57
+ for (const rel of [
58
+ 'attached-to',
59
+ 'supersedes',
60
+ 'superseded-by',
61
+ 'related-task',
62
+ 'linked-decision',
63
+ 'derived-from',
64
+ ] as const) {
65
+ expect(provenanceEdgeRelationSchema.parse(rel)).toBe(rel);
66
+ }
67
+ });
68
+
69
+ it('rejects an unknown edge relation', () => {
70
+ expect(() => provenanceEdgeRelationSchema.parse('mentions')).toThrow();
71
+ });
72
+ });
73
+
74
+ describe('docLifecycleStatusSchema', () => {
75
+ it('accepts every canonical lifecycle status', () => {
76
+ for (const s of ['active', 'superseded', 'archived', 'draft'] as const) {
77
+ expect(docLifecycleStatusSchema.parse(s)).toBe(s);
78
+ }
79
+ });
80
+
81
+ it('rejects an unknown lifecycle status', () => {
82
+ expect(() => docLifecycleStatusSchema.parse('retired')).toThrow();
83
+ });
84
+ });
85
+
86
+ // ─── Node variants ────────────────────────────────────────────────────────────
87
+
88
+ describe('provenanceDocNodeSchema', () => {
89
+ it('round-trips a minimal doc node', () => {
90
+ const node: ProvenanceDocNode = {
91
+ kind: 'doc',
92
+ id: 'adr-078-docs-provenance-graph',
93
+ slug: 'adr-078-docs-provenance-graph',
94
+ docKind: 'adr',
95
+ title: 'ADR 078 — Docs Provenance Graph',
96
+ lifecycleStatus: 'active',
97
+ publishedAt: '2026-05-22T18:00:00.000Z',
98
+ };
99
+ const parsed = provenanceDocNodeSchema.parse(node);
100
+ expect(parsed).toEqual(node);
101
+ });
102
+
103
+ it('round-trips a superseded doc node with optional fields', () => {
104
+ const node: ProvenanceDocNode = {
105
+ kind: 'doc',
106
+ id: 'adr-066-task-creation-axes',
107
+ slug: 'adr-066-task-creation-axes',
108
+ docKind: 'adr',
109
+ title: 'ADR 066 — Task Creation Axes',
110
+ lifecycleStatus: 'superseded',
111
+ publishedAt: '2026-03-01T00:00:00.000Z',
112
+ supersededAt: '2026-05-22T18:00:00.000Z',
113
+ summary: 'Original 3-axis task creation model',
114
+ metadata: { author: 'cleo-prime' },
115
+ };
116
+ const parsed = provenanceDocNodeSchema.parse(node);
117
+ expect(parsed).toEqual(node);
118
+ });
119
+
120
+ it('rejects a doc node with the wrong discriminator', () => {
121
+ expect(() =>
122
+ provenanceDocNodeSchema.parse({
123
+ kind: 'task',
124
+ id: 'T1',
125
+ title: 't',
126
+ slug: 's',
127
+ docKind: 'adr',
128
+ lifecycleStatus: 'active',
129
+ publishedAt: '2026-05-22T18:00:00.000Z',
130
+ }),
131
+ ).toThrow();
132
+ });
133
+ });
134
+
135
+ describe('provenanceNodeSchema (discriminated union)', () => {
136
+ it('parses a task node', () => {
137
+ const node: ProvenanceTaskNode = {
138
+ kind: 'task',
139
+ id: 'T10166',
140
+ title: 'C9: Provenance graph contract',
141
+ taskType: 'task',
142
+ status: 'in_progress',
143
+ };
144
+ expect(provenanceNodeSchema.parse(node)).toEqual(node);
145
+ });
146
+
147
+ it('parses a decision node', () => {
148
+ const node: ProvenanceDecisionNode = {
149
+ kind: 'decision',
150
+ id: 'D-arch-001',
151
+ title: 'Adopt evidence-based gate ritual',
152
+ outcome: 'accepted',
153
+ decidedAt: '2026-04-12T14:30:00.000Z',
154
+ };
155
+ expect(provenanceNodeSchema.parse(node)).toEqual(node);
156
+ });
157
+
158
+ it('parses a session node', () => {
159
+ const node: ProvenanceSessionNode = {
160
+ kind: 'session',
161
+ id: 'ses_2026-05-22_abc123',
162
+ title: 'Session 2026-05-22 — T10166 implementation',
163
+ startedAt: '2026-05-22T18:00:00.000Z',
164
+ };
165
+ expect(provenanceNodeSchema.parse(node)).toEqual(node);
166
+ });
167
+
168
+ it('parses a memory node', () => {
169
+ const node: ProvenanceMemoryNode = {
170
+ kind: 'memory',
171
+ id: 'O-2026-05-22-001',
172
+ title: 'Observed spawn contention under parallel sessions',
173
+ memoryType: 'observation',
174
+ recordedAt: '2026-05-22T19:15:00.000Z',
175
+ };
176
+ expect(provenanceNodeSchema.parse(node)).toEqual(node);
177
+ });
178
+
179
+ it('rejects an unknown discriminator', () => {
180
+ expect(() =>
181
+ provenanceNodeSchema.parse({
182
+ kind: 'artifact',
183
+ id: 'a-1',
184
+ title: 'unknown',
185
+ }),
186
+ ).toThrow();
187
+ });
188
+ });
189
+
190
+ // ─── Edges ────────────────────────────────────────────────────────────────────
191
+
192
+ describe('provenanceEdgeSchema', () => {
193
+ it('round-trips a minimal edge', () => {
194
+ const edge: ProvenanceEdge = {
195
+ relation: 'attached-to',
196
+ from: 'adr-078-docs-provenance-graph',
197
+ fromKind: 'doc',
198
+ to: 'T10157',
199
+ toKind: 'task',
200
+ addedAt: '2026-05-22T18:00:00.000Z',
201
+ };
202
+ expect(provenanceEdgeSchema.parse(edge)).toEqual(edge);
203
+ });
204
+
205
+ it('round-trips an edge with optional summary', () => {
206
+ const edge: ProvenanceEdge = {
207
+ relation: 'supersedes',
208
+ from: 'adr-078-docs-provenance-graph',
209
+ fromKind: 'doc',
210
+ to: 'adr-066-task-creation-axes',
211
+ toKind: 'doc',
212
+ addedAt: '2026-05-22T18:00:00.000Z',
213
+ summary: 'replaces 3-axis model with full cross-entity graph',
214
+ };
215
+ expect(provenanceEdgeSchema.parse(edge)).toEqual(edge);
216
+ });
217
+
218
+ it('rejects an edge with an unknown relation', () => {
219
+ expect(() =>
220
+ provenanceEdgeSchema.parse({
221
+ relation: 'mentions',
222
+ from: 'a',
223
+ fromKind: 'doc',
224
+ to: 'b',
225
+ toKind: 'doc',
226
+ addedAt: '2026-05-22T18:00:00.000Z',
227
+ }),
228
+ ).toThrow();
229
+ });
230
+ });
231
+
232
+ // ─── Envelope ─────────────────────────────────────────────────────────────────
233
+
234
+ describe('docProvenanceResponseSchema', () => {
235
+ it('validates an empty graph', () => {
236
+ const empty: DocProvenanceResponse = {
237
+ nodes: [],
238
+ edges: [],
239
+ totalNodes: 0,
240
+ totalEdges: 0,
241
+ };
242
+ expect(docProvenanceResponseSchema.parse(empty)).toEqual(empty);
243
+ });
244
+
245
+ it('validates a single-node graph', () => {
246
+ const single: DocProvenanceResponse = {
247
+ nodes: [
248
+ {
249
+ kind: 'doc',
250
+ id: 'adr-078-docs-provenance-graph',
251
+ slug: 'adr-078-docs-provenance-graph',
252
+ docKind: 'adr',
253
+ title: 'ADR 078',
254
+ lifecycleStatus: 'active',
255
+ publishedAt: '2026-05-22T18:00:00.000Z',
256
+ },
257
+ ],
258
+ edges: [],
259
+ totalNodes: 1,
260
+ totalEdges: 0,
261
+ };
262
+ expect(docProvenanceResponseSchema.parse(single)).toEqual(single);
263
+ });
264
+
265
+ it('rejects negative totals', () => {
266
+ expect(() =>
267
+ docProvenanceResponseSchema.parse({
268
+ nodes: [],
269
+ edges: [],
270
+ totalNodes: -1,
271
+ totalEdges: 0,
272
+ }),
273
+ ).toThrow();
274
+ });
275
+
276
+ it('isDocProvenanceResponse narrows a valid envelope', () => {
277
+ const candidate: unknown = {
278
+ nodes: [],
279
+ edges: [],
280
+ totalNodes: 0,
281
+ totalEdges: 0,
282
+ };
283
+ expect(isDocProvenanceResponse(candidate)).toBe(true);
284
+ });
285
+
286
+ it('isDocProvenanceResponse rejects a non-object', () => {
287
+ expect(isDocProvenanceResponse(null)).toBe(false);
288
+ expect(isDocProvenanceResponse('graph')).toBe(false);
289
+ expect(isDocProvenanceResponse({ nodes: 'oops' })).toBe(false);
290
+ });
291
+ });
292
+
293
+ // ─── Type guards ──────────────────────────────────────────────────────────────
294
+
295
+ describe('type guards', () => {
296
+ const doc: ProvenanceDocNode = {
297
+ kind: 'doc',
298
+ id: 'adr-078-docs-provenance-graph',
299
+ slug: 'adr-078-docs-provenance-graph',
300
+ docKind: 'adr',
301
+ title: 'ADR 078',
302
+ lifecycleStatus: 'active',
303
+ publishedAt: '2026-05-22T18:00:00.000Z',
304
+ };
305
+ const task: ProvenanceTaskNode = {
306
+ kind: 'task',
307
+ id: 'T10166',
308
+ title: 'C9',
309
+ taskType: 'task',
310
+ status: 'in_progress',
311
+ };
312
+ const decision: ProvenanceDecisionNode = {
313
+ kind: 'decision',
314
+ id: 'D-arch-001',
315
+ title: 'Adopt EBG ritual',
316
+ outcome: 'accepted',
317
+ decidedAt: '2026-04-12T14:30:00.000Z',
318
+ };
319
+ const session: ProvenanceSessionNode = {
320
+ kind: 'session',
321
+ id: 'ses_x',
322
+ title: 's',
323
+ startedAt: '2026-05-22T18:00:00.000Z',
324
+ };
325
+ const memory: ProvenanceMemoryNode = {
326
+ kind: 'memory',
327
+ id: 'O-1',
328
+ title: 'm',
329
+ memoryType: 'observation',
330
+ recordedAt: '2026-05-22T18:00:00.000Z',
331
+ };
332
+
333
+ it('isProvenanceDocNode narrows correctly', () => {
334
+ expect(isProvenanceDocNode(doc)).toBe(true);
335
+ expect(isProvenanceDocNode(task)).toBe(false);
336
+ expect(isProvenanceDocNode(decision)).toBe(false);
337
+ expect(isProvenanceDocNode(session)).toBe(false);
338
+ expect(isProvenanceDocNode(memory)).toBe(false);
339
+ });
340
+
341
+ it('isProvenanceTaskNode narrows correctly', () => {
342
+ expect(isProvenanceTaskNode(task)).toBe(true);
343
+ expect(isProvenanceTaskNode(doc)).toBe(false);
344
+ });
345
+
346
+ it('isProvenanceDecisionNode narrows correctly', () => {
347
+ expect(isProvenanceDecisionNode(decision)).toBe(true);
348
+ expect(isProvenanceDecisionNode(doc)).toBe(false);
349
+ });
350
+
351
+ it('isProvenanceSessionNode narrows correctly', () => {
352
+ expect(isProvenanceSessionNode(session)).toBe(true);
353
+ expect(isProvenanceSessionNode(doc)).toBe(false);
354
+ });
355
+
356
+ it('isProvenanceMemoryNode narrows correctly', () => {
357
+ expect(isProvenanceMemoryNode(memory)).toBe(true);
358
+ expect(isProvenanceMemoryNode(doc)).toBe(false);
359
+ });
360
+ });
361
+
362
+ // ─── Snapshot: typical-sized graph (AC4) ──────────────────────────────────────
363
+
364
+ describe('typical-sized graph (AC4)', () => {
365
+ /**
366
+ * Build the AC4 fixture: 5 docs, 8 tasks, 4 decisions, 2 sessions.
367
+ * Memory nodes are not part of the AC4 fixture but the contract supports
368
+ * them — see {@link provenanceMemoryNodeSchema} tests above.
369
+ */
370
+ const buildFixture = (): DocProvenanceResponse => {
371
+ const docs: ProvenanceDocNode[] = [
372
+ {
373
+ kind: 'doc',
374
+ id: 'adr-078-docs-provenance-graph',
375
+ slug: 'adr-078-docs-provenance-graph',
376
+ docKind: 'adr',
377
+ title: 'ADR 078 — Docs Provenance Graph',
378
+ lifecycleStatus: 'active',
379
+ publishedAt: '2026-05-22T18:00:00.000Z',
380
+ },
381
+ {
382
+ kind: 'doc',
383
+ id: 'spec-t10157-e12-provenance',
384
+ slug: 'spec-t10157-e12-provenance',
385
+ docKind: 'spec',
386
+ title: 'E12 Docs Provenance Specification',
387
+ lifecycleStatus: 'active',
388
+ publishedAt: '2026-05-22T18:05:00.000Z',
389
+ },
390
+ {
391
+ kind: 'doc',
392
+ id: 'research-provenance-graph-design',
393
+ slug: 'research-provenance-graph-design',
394
+ docKind: 'research',
395
+ title: 'Research — Cross-entity provenance graph design',
396
+ lifecycleStatus: 'active',
397
+ publishedAt: '2026-05-21T10:00:00.000Z',
398
+ },
399
+ {
400
+ kind: 'doc',
401
+ id: 'adr-066-task-creation-axes',
402
+ slug: 'adr-066-task-creation-axes',
403
+ docKind: 'adr',
404
+ title: 'ADR 066 — Task Creation Axes',
405
+ lifecycleStatus: 'superseded',
406
+ publishedAt: '2026-03-01T00:00:00.000Z',
407
+ supersededAt: '2026-05-22T18:00:00.000Z',
408
+ },
409
+ {
410
+ kind: 'doc',
411
+ id: 'handoff-sg-template-config-ssot-session-1',
412
+ slug: 'handoff-sg-template-config-ssot-session-1',
413
+ docKind: 'handoff',
414
+ title: 'SG-TEMPLATE-CONFIG-SSOT handoff — session 1',
415
+ lifecycleStatus: 'active',
416
+ publishedAt: '2026-05-22T19:30:00.000Z',
417
+ },
418
+ ];
419
+ const tasks: ProvenanceTaskNode[] = [
420
+ { kind: 'task', id: 'T9855', title: 'Saga T9855', taskType: 'saga', status: 'in_progress' },
421
+ {
422
+ kind: 'task',
423
+ id: 'T10157',
424
+ title: 'E12 — Provenance Graph',
425
+ taskType: 'epic',
426
+ status: 'in_progress',
427
+ },
428
+ {
429
+ kind: 'task',
430
+ id: 'T10158',
431
+ title: 'C1 — attachments-table migration',
432
+ taskType: 'task',
433
+ status: 'done',
434
+ },
435
+ {
436
+ kind: 'task',
437
+ id: 'T10162',
438
+ title: 'C5 — supersede verb',
439
+ taskType: 'task',
440
+ status: 'pending',
441
+ },
442
+ { kind: 'task', id: 'T10164', title: 'C7 — graph CLI', taskType: 'task', status: 'pending' },
443
+ {
444
+ kind: 'task',
445
+ id: 'T10166',
446
+ title: 'C9 — provenance graph contract',
447
+ taskType: 'task',
448
+ status: 'in_progress',
449
+ },
450
+ {
451
+ kind: 'task',
452
+ id: 'T10138',
453
+ title: 'TreeResponse contract',
454
+ taskType: 'task',
455
+ status: 'done',
456
+ },
457
+ {
458
+ kind: 'task',
459
+ id: 'T10166-sub1',
460
+ title: 'Snapshot test fixture',
461
+ taskType: 'subtask',
462
+ status: 'in_progress',
463
+ },
464
+ ];
465
+ const decisions: ProvenanceDecisionNode[] = [
466
+ {
467
+ kind: 'decision',
468
+ id: 'D-prov-001',
469
+ title: 'Cross-entity graph (not doc-only)',
470
+ outcome: 'accepted',
471
+ decidedAt: '2026-05-21T12:00:00.000Z',
472
+ },
473
+ {
474
+ kind: 'decision',
475
+ id: 'D-prov-002',
476
+ title: 'Discriminated unions over inheritance',
477
+ outcome: 'accepted',
478
+ decidedAt: '2026-05-21T13:00:00.000Z',
479
+ },
480
+ {
481
+ kind: 'decision',
482
+ id: 'D-prov-003',
483
+ title: 'Edge carries fromKind/toKind for disambiguation',
484
+ outcome: 'accepted',
485
+ decidedAt: '2026-05-21T14:00:00.000Z',
486
+ },
487
+ {
488
+ kind: 'decision',
489
+ id: 'D-prov-004',
490
+ title: 'Compatible with TreeResponse renderer',
491
+ outcome: 'proposed',
492
+ decidedAt: '2026-05-22T18:00:00.000Z',
493
+ },
494
+ ];
495
+ const sessions: ProvenanceSessionNode[] = [
496
+ {
497
+ kind: 'session',
498
+ id: 'ses_2026-05-21_research',
499
+ title: 'Research session — provenance graph design',
500
+ startedAt: '2026-05-21T10:00:00.000Z',
501
+ endedAt: '2026-05-21T14:30:00.000Z',
502
+ },
503
+ {
504
+ kind: 'session',
505
+ id: 'ses_2026-05-22_impl',
506
+ title: 'Implementation session — T10166',
507
+ startedAt: '2026-05-22T18:00:00.000Z',
508
+ },
509
+ ];
510
+ const nodes: ProvenanceNode[] = [...docs, ...tasks, ...decisions, ...sessions];
511
+ const edges: ProvenanceEdge[] = [
512
+ // ADR-078 supersedes ADR-066
513
+ {
514
+ relation: 'supersedes',
515
+ from: 'adr-078-docs-provenance-graph',
516
+ fromKind: 'doc',
517
+ to: 'adr-066-task-creation-axes',
518
+ toKind: 'doc',
519
+ addedAt: '2026-05-22T18:00:00.000Z',
520
+ },
521
+ {
522
+ relation: 'superseded-by',
523
+ from: 'adr-066-task-creation-axes',
524
+ fromKind: 'doc',
525
+ to: 'adr-078-docs-provenance-graph',
526
+ toKind: 'doc',
527
+ addedAt: '2026-05-22T18:00:00.000Z',
528
+ },
529
+ // ADR-078 attached-to T10157
530
+ {
531
+ relation: 'attached-to',
532
+ from: 'adr-078-docs-provenance-graph',
533
+ fromKind: 'doc',
534
+ to: 'T10157',
535
+ toKind: 'task',
536
+ addedAt: '2026-05-22T18:00:00.000Z',
537
+ },
538
+ // spec attached-to T10157
539
+ {
540
+ relation: 'attached-to',
541
+ from: 'spec-t10157-e12-provenance',
542
+ fromKind: 'doc',
543
+ to: 'T10157',
544
+ toKind: 'task',
545
+ addedAt: '2026-05-22T18:05:00.000Z',
546
+ },
547
+ // research attached-to T10157
548
+ {
549
+ relation: 'attached-to',
550
+ from: 'research-provenance-graph-design',
551
+ fromKind: 'doc',
552
+ to: 'T10157',
553
+ toKind: 'task',
554
+ addedAt: '2026-05-21T10:00:00.000Z',
555
+ },
556
+ // handoff attached-to T9855
557
+ {
558
+ relation: 'attached-to',
559
+ from: 'handoff-sg-template-config-ssot-session-1',
560
+ fromKind: 'doc',
561
+ to: 'T9855',
562
+ toKind: 'task',
563
+ addedAt: '2026-05-22T19:30:00.000Z',
564
+ },
565
+ // T10157 related-task T9855
566
+ {
567
+ relation: 'related-task',
568
+ from: 'T10157',
569
+ fromKind: 'task',
570
+ to: 'T9855',
571
+ toKind: 'task',
572
+ addedAt: '2026-05-22T18:00:00.000Z',
573
+ },
574
+ // T10166 related-task T10138 (TreeResponse compatibility)
575
+ {
576
+ relation: 'related-task',
577
+ from: 'T10166',
578
+ fromKind: 'task',
579
+ to: 'T10138',
580
+ toKind: 'task',
581
+ addedAt: '2026-05-22T18:00:00.000Z',
582
+ summary: 'Compatible with TreeResponse renderer (AC3)',
583
+ },
584
+ // ADR-078 linked-decision D-prov-001
585
+ {
586
+ relation: 'linked-decision',
587
+ from: 'adr-078-docs-provenance-graph',
588
+ fromKind: 'doc',
589
+ to: 'D-prov-001',
590
+ toKind: 'decision',
591
+ addedAt: '2026-05-21T12:00:00.000Z',
592
+ },
593
+ // spec linked-decision D-prov-002
594
+ {
595
+ relation: 'linked-decision',
596
+ from: 'spec-t10157-e12-provenance',
597
+ fromKind: 'doc',
598
+ to: 'D-prov-002',
599
+ toKind: 'decision',
600
+ addedAt: '2026-05-21T13:00:00.000Z',
601
+ },
602
+ // research derived-from session ses_2026-05-21_research
603
+ {
604
+ relation: 'derived-from',
605
+ from: 'research-provenance-graph-design',
606
+ fromKind: 'doc',
607
+ to: 'ses_2026-05-21_research',
608
+ toKind: 'session',
609
+ addedAt: '2026-05-21T10:00:00.000Z',
610
+ },
611
+ // handoff derived-from session ses_2026-05-22_impl
612
+ {
613
+ relation: 'derived-from',
614
+ from: 'handoff-sg-template-config-ssot-session-1',
615
+ fromKind: 'doc',
616
+ to: 'ses_2026-05-22_impl',
617
+ toKind: 'session',
618
+ addedAt: '2026-05-22T19:30:00.000Z',
619
+ },
620
+ ];
621
+ return {
622
+ nodes,
623
+ edges,
624
+ totalNodes: nodes.length,
625
+ totalEdges: edges.length,
626
+ };
627
+ };
628
+
629
+ it('matches the AC4 fixture snapshot (5 docs + 8 tasks + 4 decisions + 2 sessions)', () => {
630
+ const graph = buildFixture();
631
+
632
+ // Sanity: AC4 cardinalities exactly.
633
+ expect(graph.nodes.filter((n) => n.kind === 'doc')).toHaveLength(5);
634
+ expect(graph.nodes.filter((n) => n.kind === 'task')).toHaveLength(8);
635
+ expect(graph.nodes.filter((n) => n.kind === 'decision')).toHaveLength(4);
636
+ expect(graph.nodes.filter((n) => n.kind === 'session')).toHaveLength(2);
637
+ expect(graph.totalNodes).toBe(graph.nodes.length);
638
+ expect(graph.totalEdges).toBe(graph.edges.length);
639
+
640
+ // Round-trip through the Zod envelope schema — guarantees the snapshot
641
+ // is also wire-valid.
642
+ const parsed = docProvenanceResponseSchema.parse(graph);
643
+ expect(parsed.totalNodes).toBe(graph.totalNodes);
644
+ expect(parsed.totalEdges).toBe(graph.totalEdges);
645
+
646
+ expect(graph).toMatchSnapshot();
647
+ });
648
+ });