@cleocode/contracts 2026.5.122 → 2026.5.123
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.
- package/dist/__tests__/completion-operation-contracts.test.d.ts +8 -0
- package/dist/__tests__/completion-operation-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/completion-operation-contracts.test.js +114 -0
- package/dist/__tests__/completion-operation-contracts.test.js.map +1 -0
- package/dist/__tests__/operation-envelope-validation.test.d.ts +8 -0
- package/dist/__tests__/operation-envelope-validation.test.d.ts.map +1 -0
- package/dist/__tests__/operation-envelope-validation.test.js +105 -0
- package/dist/__tests__/operation-envelope-validation.test.js.map +1 -0
- package/dist/__tests__/satisfies-atom.test.js +21 -3
- package/dist/__tests__/satisfies-atom.test.js.map +1 -1
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts +8 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.js +188 -0
- package/dist/__tests__/workgraph-context-scaffold-contracts.test.js.map +1 -0
- package/dist/__tests__/workgraph-operation-contracts.test.d.ts +8 -0
- package/dist/__tests__/workgraph-operation-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/workgraph-operation-contracts.test.js +115 -0
- package/dist/__tests__/workgraph-operation-contracts.test.js.map +1 -0
- package/dist/data-accessor.d.ts +77 -0
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/dispatch/__tests__/docs-update-registry.test.d.ts +2 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.d.ts.map +1 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.js +27 -0
- package/dist/dispatch/__tests__/docs-update-registry.test.js.map +1 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts +2 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.d.ts.map +1 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js +265 -0
- package/dist/dispatch/__tests__/operation-envelope-snapshots.test.js.map +1 -0
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +487 -7
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +4 -4
- package/dist/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +9 -9
- package/dist/evidence-atom-schema.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/operation-envelope-validation.d.ts +79 -0
- package/dist/operation-envelope-validation.d.ts.map +1 -0
- package/dist/operation-envelope-validation.js +219 -0
- package/dist/operation-envelope-validation.js.map +1 -0
- package/dist/operations/__tests__/agent-operation-contract.test.d.ts +2 -0
- package/dist/operations/__tests__/agent-operation-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/agent-operation-contract.test.js +128 -0
- package/dist/operations/__tests__/agent-operation-contract.test.js.map +1 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js +15 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -1
- package/dist/operations/agent-operation-contract.d.ts +227 -0
- package/dist/operations/agent-operation-contract.d.ts.map +1 -0
- package/dist/operations/agent-operation-contract.js +38 -0
- package/dist/operations/agent-operation-contract.js.map +1 -0
- package/dist/operations/docs.d.ts +19 -1
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/docs.js +20 -0
- package/dist/operations/docs.js.map +1 -1
- package/dist/operations/focus.d.ts +1 -1
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +1 -0
- package/dist/operations/index.js.map +1 -1
- package/dist/operations/orchestrate.d.ts +66 -0
- package/dist/operations/orchestrate.d.ts.map +1 -1
- package/dist/operations/tasks.d.ts +568 -11
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +22 -3
- package/dist/operations/tasks.js.map +1 -1
- package/dist/task-record.d.ts +15 -0
- package/dist/task-record.d.ts.map +1 -1
- package/dist/task.d.ts +15 -6
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js.map +1 -1
- package/dist/tasks.d.ts +944 -0
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +198 -1
- package/dist/tasks.js.map +1 -1
- package/dist/workgraph.d.ts +1950 -0
- package/dist/workgraph.d.ts.map +1 -0
- package/dist/workgraph.js +449 -0
- package/dist/workgraph.js.map +1 -0
- package/package.json +10 -2
- package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
- package/src/__tests__/completion-operation-contracts.test.ts +154 -0
- package/src/__tests__/operation-envelope-validation.test.ts +140 -0
- package/src/__tests__/satisfies-atom.test.ts +23 -3
- package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
- package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
- package/src/data-accessor.ts +94 -1
- package/src/dispatch/__tests__/docs-update-registry.test.ts +35 -0
- package/src/dispatch/__tests__/operation-envelope-snapshots.test.ts +283 -0
- package/src/dispatch/operations-registry.ts +502 -7
- package/src/evidence-atom-schema.ts +10 -10
- package/src/index.ts +193 -2
- package/src/operation-envelope-validation.ts +319 -0
- package/src/operations/__tests__/agent-operation-contract.test.ts +161 -0
- package/src/operations/__tests__/tasks-input-contracts.test.ts +30 -0
- package/src/operations/agent-operation-contract.ts +253 -0
- package/src/operations/docs.ts +27 -7
- package/src/operations/focus.ts +1 -1
- package/src/operations/index.ts +1 -0
- package/src/operations/orchestrate.ts +71 -0
- package/src/operations/tasks.ts +612 -15
- package/src/task-record.ts +16 -0
- package/src/task.ts +16 -6
- package/src/tasks.ts +558 -0
- 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-
|
|
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
|
+
});
|