@cleocode/contracts 2026.5.122 → 2026.5.124

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__/completion-operation-contracts.test.d.ts +8 -0
  2. package/dist/__tests__/completion-operation-contracts.test.d.ts.map +1 -0
  3. package/dist/__tests__/completion-operation-contracts.test.js +114 -0
  4. package/dist/__tests__/completion-operation-contracts.test.js.map +1 -0
  5. package/dist/__tests__/operation-envelope-validation.test.d.ts +8 -0
  6. package/dist/__tests__/operation-envelope-validation.test.d.ts.map +1 -0
  7. package/dist/__tests__/operation-envelope-validation.test.js +105 -0
  8. package/dist/__tests__/operation-envelope-validation.test.js.map +1 -0
  9. package/dist/__tests__/satisfies-atom.test.js +21 -3
  10. package/dist/__tests__/satisfies-atom.test.js.map +1 -1
  11. package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts +8 -0
  12. package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts.map +1 -0
  13. package/dist/__tests__/workgraph-context-scaffold-contracts.test.js +188 -0
  14. package/dist/__tests__/workgraph-context-scaffold-contracts.test.js.map +1 -0
  15. package/dist/__tests__/workgraph-operation-contracts.test.d.ts +8 -0
  16. package/dist/__tests__/workgraph-operation-contracts.test.d.ts.map +1 -0
  17. package/dist/__tests__/workgraph-operation-contracts.test.js +115 -0
  18. package/dist/__tests__/workgraph-operation-contracts.test.js.map +1 -0
  19. package/dist/data-accessor.d.ts +77 -0
  20. package/dist/data-accessor.d.ts.map +1 -1
  21. package/dist/data-accessor.js.map +1 -1
  22. package/dist/dispatch/__tests__/docs-update-registry.test.d.ts +2 -0
  23. package/dist/dispatch/__tests__/docs-update-registry.test.d.ts.map +1 -0
  24. package/dist/dispatch/__tests__/docs-update-registry.test.js +27 -0
  25. package/dist/dispatch/__tests__/docs-update-registry.test.js.map +1 -0
  26. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts +2 -0
  27. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts.map +1 -0
  28. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js +271 -0
  29. package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js.map +1 -0
  30. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  31. package/dist/dispatch/operations-registry.js +487 -7
  32. package/dist/dispatch/operations-registry.js.map +1 -1
  33. package/dist/evidence-atom-schema.d.ts +4 -4
  34. package/dist/evidence-atom-schema.d.ts.map +1 -1
  35. package/dist/evidence-atom-schema.js +9 -9
  36. package/dist/evidence-atom-schema.js.map +1 -1
  37. package/dist/index.d.ts +12 -6
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +8 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/operation-envelope-validation.d.ts +79 -0
  42. package/dist/operation-envelope-validation.d.ts.map +1 -0
  43. package/dist/operation-envelope-validation.js +219 -0
  44. package/dist/operation-envelope-validation.js.map +1 -0
  45. package/dist/operations/__tests__/agent-operation-contract.test.d.ts +2 -0
  46. package/dist/operations/__tests__/agent-operation-contract.test.d.ts.map +1 -0
  47. package/dist/operations/__tests__/agent-operation-contract.test.js +128 -0
  48. package/dist/operations/__tests__/agent-operation-contract.test.js.map +1 -0
  49. package/dist/operations/__tests__/tasks-input-contracts.test.js +15 -0
  50. package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -1
  51. package/dist/operations/agent-operation-contract.d.ts +227 -0
  52. package/dist/operations/agent-operation-contract.d.ts.map +1 -0
  53. package/dist/operations/agent-operation-contract.js +38 -0
  54. package/dist/operations/agent-operation-contract.js.map +1 -0
  55. package/dist/operations/docs.d.ts +19 -1
  56. package/dist/operations/docs.d.ts.map +1 -1
  57. package/dist/operations/docs.js +20 -0
  58. package/dist/operations/docs.js.map +1 -1
  59. package/dist/operations/focus.d.ts +1 -1
  60. package/dist/operations/index.d.ts +1 -0
  61. package/dist/operations/index.d.ts.map +1 -1
  62. package/dist/operations/index.js +1 -0
  63. package/dist/operations/index.js.map +1 -1
  64. package/dist/operations/orchestrate.d.ts +66 -0
  65. package/dist/operations/orchestrate.d.ts.map +1 -1
  66. package/dist/operations/tasks.d.ts +568 -11
  67. package/dist/operations/tasks.d.ts.map +1 -1
  68. package/dist/operations/tasks.js +22 -3
  69. package/dist/operations/tasks.js.map +1 -1
  70. package/dist/task-record.d.ts +15 -0
  71. package/dist/task-record.d.ts.map +1 -1
  72. package/dist/task.d.ts +15 -6
  73. package/dist/task.d.ts.map +1 -1
  74. package/dist/task.js.map +1 -1
  75. package/dist/tasks.d.ts +944 -0
  76. package/dist/tasks.d.ts.map +1 -1
  77. package/dist/tasks.js +198 -1
  78. package/dist/tasks.js.map +1 -1
  79. package/dist/workgraph.d.ts +1950 -0
  80. package/dist/workgraph.d.ts.map +1 -0
  81. package/dist/workgraph.js +449 -0
  82. package/dist/workgraph.js.map +1 -0
  83. package/package.json +10 -2
  84. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  85. package/src/__tests__/completion-operation-contracts.test.ts +154 -0
  86. package/src/__tests__/operation-envelope-validation.test.ts +140 -0
  87. package/src/__tests__/satisfies-atom.test.ts +23 -3
  88. package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
  89. package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
  90. package/src/data-accessor.ts +94 -1
  91. package/src/dispatch/__tests__/docs-update-registry.test.ts +35 -0
  92. package/src/dispatch/__tests__/operation-envelope-snapshots.test.ts +289 -0
  93. package/src/dispatch/operations-registry.ts +502 -7
  94. package/src/evidence-atom-schema.ts +10 -10
  95. package/src/index.ts +193 -2
  96. package/src/operation-envelope-validation.ts +319 -0
  97. package/src/operations/__tests__/agent-operation-contract.test.ts +161 -0
  98. package/src/operations/__tests__/tasks-input-contracts.test.ts +30 -0
  99. package/src/operations/agent-operation-contract.ts +253 -0
  100. package/src/operations/docs.ts +27 -7
  101. package/src/operations/focus.ts +1 -1
  102. package/src/operations/index.ts +1 -0
  103. package/src/operations/orchestrate.ts +71 -0
  104. package/src/operations/tasks.ts +612 -15
  105. package/src/task-record.ts +16 -0
  106. package/src/task.ts +16 -6
  107. package/src/tasks.ts +558 -0
  108. package/src/workgraph.ts +1153 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Contract tests for completion.* and projection.repair dispatch shapes.
3
+ *
4
+ * @task T10607
5
+ * @saga T10538
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import { OPERATIONS } from '../dispatch/operations-registry.js';
10
+ import {
11
+ completionEvaluateParamsSchema,
12
+ completionEvaluationSchema,
13
+ completionExplainParamsSchema,
14
+ completionExplanationSchema,
15
+ completionListParamsSchema,
16
+ completionListResultSchema,
17
+ completionProjectionRepairErrorSchema,
18
+ completionProjectionRepairParamsSchema,
19
+ completionProjectionRepairResultSchema,
20
+ unsatisfiedCompletionCriterionSchema,
21
+ } from '../tasks.js';
22
+
23
+ const requiredOperations = [
24
+ 'completion.list',
25
+ 'completion.evaluate',
26
+ 'completion.explain',
27
+ 'projection.repair',
28
+ ] as const;
29
+
30
+ function operation(name: (typeof requiredOperations)[number]) {
31
+ const def = OPERATIONS.find(
32
+ (candidate) => candidate.domain === 'tasks' && candidate.operation === name,
33
+ );
34
+ if (def === undefined) throw new Error(`missing tasks.${name}`);
35
+ return def;
36
+ }
37
+
38
+ const unsatisfiedCriterion = {
39
+ acId: 'ac-1',
40
+ alias: 'AC1',
41
+ text: 'typed AC result schemas added',
42
+ kind: 'evidence_bound',
43
+ status: 'unsatisfied',
44
+ reason: 'missing_evidence_binding',
45
+ evidenceBindings: 0,
46
+ } as const;
47
+
48
+ const totals = { criteria: 1, satisfied: 0, unsatisfied: 1, waived: 0, replaced: 0 };
49
+
50
+ describe('Completion operation contract schemas', () => {
51
+ it('validates typed AC result, unsatisfied criterion, evaluate, and explain shapes', () => {
52
+ expect(unsatisfiedCompletionCriterionSchema.parse(unsatisfiedCriterion).reason).toBe(
53
+ 'missing_evidence_binding',
54
+ );
55
+
56
+ expect(
57
+ completionEvaluationSchema.parse({
58
+ taskId: 'T10607',
59
+ taskStatus: 'pending',
60
+ ready: false,
61
+ stale: false,
62
+ staleReasons: [],
63
+ satisfied: [],
64
+ unsatisfied: [unsatisfiedCriterion],
65
+ waived: [],
66
+ replaced: [],
67
+ totals,
68
+ }).unsatisfied,
69
+ ).toHaveLength(1);
70
+
71
+ expect(
72
+ completionExplanationSchema.parse({
73
+ taskId: 'T10607',
74
+ ready: false,
75
+ stale: true,
76
+ summary: 'Task has one unsatisfied completion criterion.',
77
+ blockers: [unsatisfiedCriterion],
78
+ }).blockers[0]?.status,
79
+ ).toBe('unsatisfied');
80
+ });
81
+
82
+ it('validates params/results for completion.list/evaluate/explain/projection.repair', () => {
83
+ expect(completionListParamsSchema.parse({ taskId: 'T10607', status: 'unsatisfied' })).toEqual({
84
+ taskId: 'T10607',
85
+ status: 'unsatisfied',
86
+ });
87
+ expect(
88
+ completionListResultSchema.parse({
89
+ taskId: 'T10607',
90
+ criteria: [unsatisfiedCriterion],
91
+ totals,
92
+ }).criteria[0]?.alias,
93
+ ).toBe('AC1');
94
+
95
+ expect(
96
+ completionEvaluateParamsSchema.parse({
97
+ taskId: 'T10607',
98
+ includeContext: true,
99
+ limit: 10,
100
+ relationDepth: 1,
101
+ }),
102
+ ).toEqual({ taskId: 'T10607', includeContext: true, limit: 10, relationDepth: 1 });
103
+ expect(completionExplainParamsSchema.parse({ taskId: 'T10607', since: '2026-05-26' })).toEqual({
104
+ taskId: 'T10607',
105
+ since: '2026-05-26',
106
+ });
107
+
108
+ expect(
109
+ completionProjectionRepairErrorSchema.parse({
110
+ code: 'criteria_missing',
111
+ message: 'Projected AC row no longer exists.',
112
+ taskId: 'T10607',
113
+ acId: 'ac-1',
114
+ }).code,
115
+ ).toBe('criteria_missing');
116
+ expect(
117
+ completionProjectionRepairParamsSchema.parse({ taskId: 'T10607', dryRun: true }),
118
+ ).toEqual({
119
+ taskId: 'T10607',
120
+ dryRun: true,
121
+ });
122
+ expect(
123
+ completionProjectionRepairResultSchema.parse({
124
+ taskId: 'T10607',
125
+ repaired: false,
126
+ dryRun: true,
127
+ staleBefore: true,
128
+ staleAfter: true,
129
+ errors: [
130
+ {
131
+ code: 'repair_conflict',
132
+ message: 'Concurrent projection change detected.',
133
+ taskId: 'T10607',
134
+ },
135
+ ],
136
+ }).errors[0]?.code,
137
+ ).toBe('repair_conflict');
138
+ });
139
+
140
+ it('publishes operation metadata with taskId requirements and correct gateways', () => {
141
+ for (const name of requiredOperations) {
142
+ const def = operation(name);
143
+ expect(def.domain).toBe('tasks');
144
+ expect(def.idempotent).toBe(true);
145
+ expect(def.requiredParams).toEqual(['taskId']);
146
+ expect(def.params?.map((param) => param.name)).toContain('taskId');
147
+ }
148
+
149
+ expect(operation('completion.list').gateway).toBe('query');
150
+ expect(operation('completion.evaluate').gateway).toBe('query');
151
+ expect(operation('completion.explain').gateway).toBe('query');
152
+ expect(operation('projection.repair').gateway).toBe('mutate');
153
+ });
154
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Contract tests for operation-aware LAFS envelope validation.
3
+ *
4
+ * @task T10610
5
+ * @saga T10538
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import type { LAFSEnvelope } from '../lafs.js';
10
+ import {
11
+ E_LAFS_OPERATION_ERROR_SHAPE,
12
+ E_LAFS_OPERATION_RESULT_SCHEMA,
13
+ E_LAFS_OPERATION_UNREGISTERED,
14
+ validateOperationEnvelope,
15
+ } from '../operation-envelope-validation.js';
16
+ import { tasksTreeResultSchema } from '../workgraph.js';
17
+
18
+ const baseMeta: LAFSEnvelope['_meta'] = {
19
+ specVersion: '1.0.0',
20
+ schemaVersion: '1.0.0',
21
+ timestamp: '2026-05-26T10:00:00.000Z',
22
+ operation: 'tasks.frontier',
23
+ requestId: 'req-T10610',
24
+ transport: 'cli',
25
+ strict: true,
26
+ mvi: 'standard',
27
+ contextVersion: 1,
28
+ };
29
+
30
+ function successEnvelope(
31
+ operation: string,
32
+ result: NonNullable<LAFSEnvelope['result']>,
33
+ ): LAFSEnvelope {
34
+ return {
35
+ $schema: 'https://lafs.dev/schemas/v1/envelope.schema.json',
36
+ _meta: { ...baseMeta, operation },
37
+ success: true,
38
+ result,
39
+ error: null,
40
+ };
41
+ }
42
+
43
+ function failureEnvelope(operation = 'tasks.frontier'): LAFSEnvelope {
44
+ return {
45
+ $schema: 'https://lafs.dev/schemas/v1/envelope.schema.json',
46
+ _meta: { ...baseMeta, operation },
47
+ success: false,
48
+ result: null,
49
+ error: {
50
+ code: 'E_CONTRACT',
51
+ message: 'contract failed',
52
+ category: 'CONTRACT',
53
+ retryable: false,
54
+ retryAfterMs: null,
55
+ details: { taskId: 'T10610' },
56
+ agentAction: 'stop',
57
+ },
58
+ };
59
+ }
60
+
61
+ const frontierResult = {
62
+ rootId: 'T10538',
63
+ groups: { ready: [], blocked: [], blockedBy: [] },
64
+ };
65
+
66
+ describe('operation-aware LAFS envelope validation', () => {
67
+ it('checks _meta.operation against the canonical operations registry', () => {
68
+ const valid = validateOperationEnvelope(successEnvelope('tasks.frontier', frontierResult));
69
+ expect(valid.valid).toBe(true);
70
+ expect(valid.operation?.domain).toBe('tasks');
71
+ expect(valid.operation?.operation).toBe('frontier');
72
+
73
+ const invalid = validateOperationEnvelope(
74
+ successEnvelope('tasks.not-a-real-operation', frontierResult),
75
+ );
76
+ expect(invalid.valid).toBe(false);
77
+ expect(invalid.issues).toContainEqual(
78
+ expect.objectContaining({
79
+ path: '/_meta/operation',
80
+ code: E_LAFS_OPERATION_UNREGISTERED,
81
+ }),
82
+ );
83
+ });
84
+
85
+ it('validates successful payloads against the operation result schema', () => {
86
+ const invalid = validateOperationEnvelope(
87
+ successEnvelope('tasks.frontier', { rootId: 'T10538', groups: { ready: [] } }),
88
+ );
89
+ expect(invalid.valid).toBe(false);
90
+ expect(invalid.resultSchemaOperation).toBe('tasks.frontier');
91
+ expect(invalid.issues.some((issue) => issue.code === E_LAFS_OPERATION_RESULT_SCHEMA)).toBe(
92
+ true,
93
+ );
94
+
95
+ const valid = validateOperationEnvelope(successEnvelope('tasks.frontier', frontierResult));
96
+ expect(valid.valid).toBe(true);
97
+ expect(valid.resultSchemaOperation).toBe('tasks.frontier');
98
+ });
99
+
100
+ it('enforces canonical error-envelope shape before result schemas', () => {
101
+ const validFailure = validateOperationEnvelope(failureEnvelope());
102
+ expect(validFailure.valid).toBe(true);
103
+ expect(validFailure.resultSchemaOperation).toBe('tasks.frontier');
104
+
105
+ const invalidFailure = validateOperationEnvelope({
106
+ ...failureEnvelope(),
107
+ result: frontierResult,
108
+ });
109
+ expect(invalidFailure.valid).toBe(false);
110
+ expect(invalidFailure.issues).toContainEqual(
111
+ expect.objectContaining({
112
+ path: '/result',
113
+ code: E_LAFS_OPERATION_ERROR_SHAPE,
114
+ }),
115
+ );
116
+ });
117
+
118
+ it('rejects relation groups edges in hierarchy tree result contracts', () => {
119
+ const result = tasksTreeResultSchema.safeParse({
120
+ rootId: 'T10538',
121
+ nodes: [],
122
+ edges: [{ fromId: 'T10538', toId: 'T10545', kind: 'groups', source: 'relation' }],
123
+ pageInfo: { hasMore: false },
124
+ });
125
+ expect(result.success).toBe(false);
126
+
127
+ const envelope = validateOperationEnvelope(
128
+ successEnvelope('tasks.tree', {
129
+ rootId: 'T10538',
130
+ nodes: [],
131
+ edges: [{ fromId: 'T10538', toId: 'T10545', kind: 'groups', source: 'relation' }],
132
+ pageInfo: { hasMore: false },
133
+ }),
134
+ );
135
+ expect(envelope.valid).toBe(false);
136
+ expect(envelope.issues.some((issue) => issue.code === E_LAFS_OPERATION_RESULT_SCHEMA)).toBe(
137
+ true,
138
+ );
139
+ });
140
+ });
@@ -58,17 +58,18 @@ describe('satisfies atom regexes (ADR-079-r2 §2.1)', () => {
58
58
  });
59
59
 
60
60
  describe('AC_UUID_REGEX', () => {
61
- it('accepts strict lowercase UUIDv4', () => {
61
+ it('accepts strict lowercase UUIDv4 and deterministic UUIDv5-shaped AC ids', () => {
62
62
  expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-abcd-ef1234567890')).toBe(true);
63
63
  expect(AC_UUID_REGEX.test('00000000-0000-4000-8000-000000000000')).toBe(true);
64
+ expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-5890-abcd-ef1234567890')).toBe(true);
64
65
  });
65
66
  it('rejects mixed-case', () => {
66
67
  // Council-mandated strictness (ADR-079-r2 §2.2): "Validators MUST
67
68
  // reject mixed-case to prevent silent dedupe failures."
68
69
  expect(AC_UUID_REGEX.test('A1B2C3D4-5E6F-4890-ABCD-EF1234567890')).toBe(false);
69
70
  });
70
- it('rejects wrong version nibble (not 4)', () => {
71
- expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-5890-abcd-ef1234567890')).toBe(false);
71
+ it('rejects wrong version nibble (not 4/5)', () => {
72
+ expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-6890-abcd-ef1234567890')).toBe(false);
72
73
  });
73
74
  it('rejects wrong variant nibble (not 8/9/a/b)', () => {
74
75
  expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-cbcd-ef1234567890')).toBe(false);
@@ -126,6 +127,15 @@ describe('satisfiesAtomSchema (ADR-079-r2 §5.2)', () => {
126
127
  expect(a.versionPin).toBeUndefined();
127
128
  });
128
129
 
130
+ it('accepts deterministic UUIDv5-shaped AC id form without version-pin', () => {
131
+ const a = satisfiesAtomSchema.parse({
132
+ kind: 'satisfies',
133
+ targetTaskId: 'T10495',
134
+ targetAcId: '8f4a2c1e-b09d-5f6a-9c3e-7a1d4f8c0b2e',
135
+ });
136
+ expect(a.targetAcId).toBe('8f4a2c1e-b09d-5f6a-9c3e-7a1d4f8c0b2e');
137
+ });
138
+
129
139
  it('accepts alias form without version-pin', () => {
130
140
  const a = satisfiesAtomSchema.parse({
131
141
  kind: 'satisfies',
@@ -210,6 +220,16 @@ describe('parseEvidenceString — satisfies atom (T10506)', () => {
210
220
  });
211
221
  });
212
222
 
223
+ it('parses deterministic UUIDv5-shaped AC id form', () => {
224
+ const atoms = parseEvidenceString('satisfies:T10495#8f4a2c1e-b09d-5f6a-9c3e-7a1d4f8c0b2e');
225
+ expect(atoms).toHaveLength(1);
226
+ expect(atoms[0]).toEqual({
227
+ kind: 'satisfies',
228
+ targetTaskId: 'T10495',
229
+ targetAcId: '8f4a2c1e-b09d-5f6a-9c3e-7a1d4f8c0b2e',
230
+ });
231
+ });
232
+
213
233
  it('parses alias form', () => {
214
234
  const atoms = parseEvidenceString('satisfies:T10495#AC2');
215
235
  expect(atoms).toHaveLength(1);
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Contract tests for bounded WorkGraph context packs, readiness, and scaffold envelopes.
3
+ *
4
+ * @task T10609
5
+ * @saga T10538
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import {
10
+ workGraphContextPackParamsSchema,
11
+ workGraphContextPackSchema,
12
+ workGraphReadinessParamsSchema,
13
+ workGraphReadinessResultSchema,
14
+ workGraphScaffoldApplyParamsSchema,
15
+ workGraphScaffoldApplyResultSchema,
16
+ workGraphScaffoldValidateParamsSchema,
17
+ workGraphScaffoldValidateResultSchema,
18
+ workGraphSliceParamsSchema,
19
+ workGraphSliceSchema,
20
+ } from '../workgraph.js';
21
+
22
+ const taskNode = {
23
+ id: 'T10609',
24
+ type: 'task',
25
+ title: 'Add contracts',
26
+ status: 'pending',
27
+ priority: 'high',
28
+ parentId: 'T10545',
29
+ } as const;
30
+
31
+ const emptyPageInfo = { hasMore: false } as const;
32
+
33
+ const readyGroups = {
34
+ ready: [
35
+ {
36
+ ...taskNode,
37
+ role: 'worker',
38
+ dependencyBlockers: [],
39
+ gateBlockers: [],
40
+ },
41
+ ],
42
+ blocked: [],
43
+ blockedBy: [],
44
+ } as const;
45
+
46
+ describe('WorkGraph bounded context and scaffold contracts', () => {
47
+ it('validates bounded context packs with graph slices, relation edges, readiness, budget, and omissions', () => {
48
+ expect(
49
+ workGraphContextPackParamsSchema.parse({
50
+ rootId: 'T10545',
51
+ tokenBudget: 1500,
52
+ includeRelations: true,
53
+ includeReadiness: true,
54
+ includeRollup: true,
55
+ limit: 25,
56
+ }),
57
+ ).toEqual({
58
+ rootId: 'T10545',
59
+ tokenBudget: 1500,
60
+ includeRelations: true,
61
+ includeReadiness: true,
62
+ includeRollup: true,
63
+ limit: 25,
64
+ });
65
+
66
+ const parsed = workGraphContextPackSchema.parse({
67
+ rootId: 'T10545',
68
+ generatedAt: '2026-05-26T10:00:00.000Z',
69
+ budget: {
70
+ tokenBudget: 1500,
71
+ estimatedTokens: 1200,
72
+ remainingTokens: 300,
73
+ truncated: true,
74
+ },
75
+ slice: {
76
+ rootId: 'T10545',
77
+ direction: 'descendants',
78
+ nodes: [taskNode],
79
+ edges: [{ fromId: 'T10545', toId: 'T10609', kind: 'contains' }],
80
+ pageInfo: { nextCursor: '0002:T10610', hasMore: true },
81
+ omissions: [
82
+ {
83
+ path: 'slice.nodes[25:]',
84
+ reason: 'budget_exceeded',
85
+ message: 'Additional nodes omitted to stay under token budget',
86
+ estimatedTokens: 400,
87
+ },
88
+ ],
89
+ },
90
+ relationEdges: {
91
+ rootId: 'T10609',
92
+ direction: 'both',
93
+ edges: [
94
+ {
95
+ source: 'dependency',
96
+ fromId: 'T10609',
97
+ toId: 'T10608',
98
+ kind: 'depends_on',
99
+ },
100
+ {
101
+ source: 'relation',
102
+ fromId: 'T10538',
103
+ toId: 'T10545',
104
+ kind: 'groups',
105
+ relationType: 'groups',
106
+ reason: 'Saga membership',
107
+ },
108
+ ],
109
+ },
110
+ readiness: {
111
+ rootId: 'T10545',
112
+ role: 'worker',
113
+ ready: true,
114
+ warnings: [],
115
+ groups: readyGroups,
116
+ },
117
+ rollup: {
118
+ rootId: 'T10545',
119
+ direct: { total: 1, byStatus: { pending: 1 }, byType: { task: 1 } },
120
+ subtree: { total: 1, byStatus: { pending: 1 }, byType: { task: 1 } },
121
+ percentDenominator: { basis: 'subtree-total', total: 1, description: 'subtree total' },
122
+ percentages: { done: 0, active: 0, blocked: 0, pending: 100, cancelled: 0 },
123
+ staleProjection: false,
124
+ projectionMismatches: [],
125
+ },
126
+ omissions: [
127
+ { path: 'rollup.history', reason: 'not_requested', message: 'History not requested' },
128
+ ],
129
+ });
130
+
131
+ expect(parsed.budget.truncated).toBe(true);
132
+ expect(parsed.relationEdges?.edges.map((edge) => edge.source)).toEqual([
133
+ 'dependency',
134
+ 'relation',
135
+ ]);
136
+ expect(parsed.readiness?.groups.ready[0]?.id).toBe('T10609');
137
+ });
138
+
139
+ it('validates graph slice and readiness envelopes independently', () => {
140
+ expect(
141
+ workGraphSliceParamsSchema.parse({
142
+ rootId: 'T10545',
143
+ direction: 'descendants',
144
+ maxDepth: 2,
145
+ includeRelations: true,
146
+ cursor: '0001:T10609',
147
+ limit: 50,
148
+ }),
149
+ ).toEqual({
150
+ rootId: 'T10545',
151
+ direction: 'descendants',
152
+ maxDepth: 2,
153
+ includeRelations: true,
154
+ cursor: '0001:T10609',
155
+ limit: 50,
156
+ });
157
+
158
+ expect(
159
+ workGraphSliceSchema.parse({
160
+ rootId: 'T10545',
161
+ direction: 'descendants',
162
+ nodes: [taskNode],
163
+ edges: [{ fromId: 'T10545', toId: 'T10609', kind: 'contains' }],
164
+ pageInfo: emptyPageInfo,
165
+ }).nodes,
166
+ ).toHaveLength(1);
167
+
168
+ expect(
169
+ workGraphReadinessParamsSchema.parse({
170
+ rootId: 'T10545',
171
+ role: 'worker',
172
+ includeGateBlockers: true,
173
+ }),
174
+ ).toEqual({ rootId: 'T10545', role: 'worker', includeGateBlockers: true });
175
+
176
+ expect(
177
+ workGraphReadinessResultSchema.parse({
178
+ rootId: 'T10545',
179
+ role: 'worker',
180
+ ready: true,
181
+ warnings: [],
182
+ groups: readyGroups,
183
+ }).ready,
184
+ ).toBe(true);
185
+ });
186
+
187
+ it('validates scaffold validate/apply contracts and hierarchy diagnostics', () => {
188
+ const nodes = [
189
+ { id: 'T10545', type: 'epic' },
190
+ { id: 'T10609', type: 'task', parentId: 'T10545' },
191
+ ] as const;
192
+ const edges = [
193
+ {
194
+ source: 'dependency',
195
+ fromId: 'T10609',
196
+ toId: 'T10608',
197
+ kind: 'depends_on',
198
+ },
199
+ ] as const;
200
+
201
+ expect(
202
+ workGraphScaffoldValidateParamsSchema.parse({ rootId: 'T10545', nodes, edges, dryRun: true }),
203
+ ).toEqual({ rootId: 'T10545', nodes, edges, dryRun: true });
204
+
205
+ expect(
206
+ workGraphScaffoldValidateResultSchema.parse({
207
+ rootId: 'T10545',
208
+ valid: true,
209
+ dryRun: true,
210
+ issues: [{ code: 'W_EXISTING_NODE', message: 'Node already exists', severity: 'warning' }],
211
+ hierarchy: { valid: true, violations: [] },
212
+ }).valid,
213
+ ).toBe(true);
214
+
215
+ expect(
216
+ workGraphScaffoldApplyParamsSchema.parse({ rootId: 'T10545', nodes, edges, apply: true }),
217
+ ).toEqual({ rootId: 'T10545', nodes, edges, apply: true });
218
+
219
+ expect(
220
+ workGraphScaffoldApplyResultSchema.parse({
221
+ rootId: 'T10545',
222
+ valid: true,
223
+ dryRun: false,
224
+ applied: true,
225
+ nodesChanged: 2,
226
+ edgesChanged: 1,
227
+ issues: [],
228
+ hierarchy: { valid: true, violations: [] },
229
+ }).edgesChanged,
230
+ ).toBe(1);
231
+ });
232
+ });