@cleocode/contracts 2026.5.121 → 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.d.ts +24 -0
- package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
- package/dist/__tests__/satisfies-atom.test.js +320 -0
- package/dist/__tests__/satisfies-atom.test.js.map +1 -0
- package/dist/__tests__/validator-types.test.d.ts +13 -0
- package/dist/__tests__/validator-types.test.d.ts.map +1 -0
- package/dist/__tests__/validator-types.test.js +293 -0
- package/dist/__tests__/validator-types.test.js.map +1 -0
- 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/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/data-accessor.d.ts +194 -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 +91 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +162 -1
- package/dist/evidence-atom-schema.js.map +1 -1
- package/dist/exit-codes.d.ts +41 -1
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +40 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +17 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- 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 +617 -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 +52 -5
- 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/validator/index.d.ts +375 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +162 -0
- package/dist/validator/index.js.map +1 -0
- 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 +383 -0
- package/src/__tests__/validator-types.test.ts +359 -0
- package/src/__tests__/workgraph-context-scaffold-contracts.test.ts +232 -0
- package/src/__tests__/workgraph-operation-contracts.test.ts +151 -0
- package/src/config.ts +58 -0
- package/src/data-accessor.ts +213 -0
- 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 +188 -1
- package/src/exit-codes.ts +42 -0
- package/src/index.ts +230 -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 +662 -15
- package/src/task-record.ts +16 -0
- package/src/task.ts +54 -5
- package/src/tasks.ts +558 -0
- package/src/validator/index.ts +385 -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
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ADR-079-r2 `satisfies:<task-id>#<ac-id>` evidence atom
|
|
3
|
+
* grammar (T10506).
|
|
4
|
+
*
|
|
5
|
+
* Coverage:
|
|
6
|
+
* - Schema-level: per-field regex validation (task-id, ac-uuid, ac-alias,
|
|
7
|
+
* version-pin) and discriminated-union acceptance.
|
|
8
|
+
* - Parser-level: full {@link parseEvidenceString} round-trips for both
|
|
9
|
+
* UUID and alias forms, mixed evidence strings, and the optional
|
|
10
|
+
* `@<version-pin>` suffix.
|
|
11
|
+
* - Malformed inputs: missing colon, missing hash, missing T-prefix,
|
|
12
|
+
* mixed-case UUID, non-UUID non-alias ac-id, malformed version-pin.
|
|
13
|
+
*
|
|
14
|
+
* This file covers PARSING ONLY per T10506 acceptance criteria. The 5-check
|
|
15
|
+
* validator semantics (target exists, target not terminal, AC exists, same-
|
|
16
|
+
* saga scope rule, alias drift detection) ship in T10507 and are tested
|
|
17
|
+
* separately in `packages/core/src/tasks/__tests__/satisfies-validator.test.ts`.
|
|
18
|
+
*
|
|
19
|
+
* @task T10506
|
|
20
|
+
* @saga T10377
|
|
21
|
+
* @adr ADR-079-r2
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
AC_ALIAS_REGEX,
|
|
28
|
+
AC_UUID_REGEX,
|
|
29
|
+
EvidenceAtomSchema,
|
|
30
|
+
EvidenceParseError,
|
|
31
|
+
parseEvidenceString,
|
|
32
|
+
SATISFIES_TASK_ID_REGEX,
|
|
33
|
+
SATISFIES_VERSION_PIN_REGEX,
|
|
34
|
+
satisfiesAtomSchema,
|
|
35
|
+
} from '../index.js';
|
|
36
|
+
|
|
37
|
+
// ─── Per-field regex parity (ADR-079-r2 §2.1 ABNF) ────────────────────────
|
|
38
|
+
|
|
39
|
+
describe('satisfies atom regexes (ADR-079-r2 §2.1)', () => {
|
|
40
|
+
describe('SATISFIES_TASK_ID_REGEX', () => {
|
|
41
|
+
it('accepts T<1-7 digits>', () => {
|
|
42
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1')).toBe(true);
|
|
43
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1234')).toBe(true);
|
|
44
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1234567')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('rejects T<8+ digits>', () => {
|
|
47
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T12345678')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('rejects missing T prefix', () => {
|
|
50
|
+
expect(SATISFIES_TASK_ID_REGEX.test('1234')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('rejects lowercase t', () => {
|
|
53
|
+
expect(SATISFIES_TASK_ID_REGEX.test('t1234')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('rejects non-digit suffix', () => {
|
|
56
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T12a')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('AC_UUID_REGEX', () => {
|
|
61
|
+
it('accepts strict lowercase UUIDv4 and deterministic UUIDv5-shaped AC ids', () => {
|
|
62
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-abcd-ef1234567890')).toBe(true);
|
|
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);
|
|
65
|
+
});
|
|
66
|
+
it('rejects mixed-case', () => {
|
|
67
|
+
// Council-mandated strictness (ADR-079-r2 §2.2): "Validators MUST
|
|
68
|
+
// reject mixed-case to prevent silent dedupe failures."
|
|
69
|
+
expect(AC_UUID_REGEX.test('A1B2C3D4-5E6F-4890-ABCD-EF1234567890')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it('rejects wrong version nibble (not 4/5)', () => {
|
|
72
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-6890-abcd-ef1234567890')).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it('rejects wrong variant nibble (not 8/9/a/b)', () => {
|
|
75
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-cbcd-ef1234567890')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
it('rejects non-UUID strings', () => {
|
|
78
|
+
expect(AC_UUID_REGEX.test('not-a-uuid')).toBe(false);
|
|
79
|
+
expect(AC_UUID_REGEX.test('a1b2c3d45e6f4890abcdef1234567890')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('AC_ALIAS_REGEX', () => {
|
|
84
|
+
it('accepts AC<1-4 digits>', () => {
|
|
85
|
+
expect(AC_ALIAS_REGEX.test('AC1')).toBe(true);
|
|
86
|
+
expect(AC_ALIAS_REGEX.test('AC9999')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('rejects AC<5+ digits>', () => {
|
|
89
|
+
expect(AC_ALIAS_REGEX.test('AC10000')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('rejects lowercase ac', () => {
|
|
92
|
+
expect(AC_ALIAS_REGEX.test('ac1')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it('rejects bare integer', () => {
|
|
95
|
+
expect(AC_ALIAS_REGEX.test('1')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('SATISFIES_VERSION_PIN_REGEX', () => {
|
|
100
|
+
it('accepts exactly 14 digits', () => {
|
|
101
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('20260524223045')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('rejects fewer than 14 digits', () => {
|
|
104
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('2026052422304')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('rejects more than 14 digits', () => {
|
|
107
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('202605242230451')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
it('rejects non-digit characters', () => {
|
|
110
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('2026-05-24T22:30')).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── Zod schema (post-parse client-side validation) ────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('satisfiesAtomSchema (ADR-079-r2 §5.2)', () => {
|
|
118
|
+
it('accepts UUID form without version-pin', () => {
|
|
119
|
+
const a = satisfiesAtomSchema.parse({
|
|
120
|
+
kind: 'satisfies',
|
|
121
|
+
targetTaskId: 'T10495',
|
|
122
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
123
|
+
});
|
|
124
|
+
expect(a.targetTaskId).toBe('T10495');
|
|
125
|
+
expect(a.targetAcId).toBe('8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e');
|
|
126
|
+
expect(a.targetAcAlias).toBeUndefined();
|
|
127
|
+
expect(a.versionPin).toBeUndefined();
|
|
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
|
+
|
|
139
|
+
it('accepts alias form without version-pin', () => {
|
|
140
|
+
const a = satisfiesAtomSchema.parse({
|
|
141
|
+
kind: 'satisfies',
|
|
142
|
+
targetTaskId: 'T10495',
|
|
143
|
+
targetAcAlias: 'AC2',
|
|
144
|
+
});
|
|
145
|
+
expect(a.targetAcAlias).toBe('AC2');
|
|
146
|
+
expect(a.targetAcId).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('accepts alias form with version-pin', () => {
|
|
150
|
+
const a = satisfiesAtomSchema.parse({
|
|
151
|
+
kind: 'satisfies',
|
|
152
|
+
targetTaskId: 'T10495',
|
|
153
|
+
targetAcAlias: 'AC2',
|
|
154
|
+
versionPin: '20260524223045',
|
|
155
|
+
});
|
|
156
|
+
expect(a.versionPin).toBe('20260524223045');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('accepts UUID form with version-pin (highest-trust)', () => {
|
|
160
|
+
const a = satisfiesAtomSchema.parse({
|
|
161
|
+
kind: 'satisfies',
|
|
162
|
+
targetTaskId: 'T10495',
|
|
163
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
164
|
+
versionPin: '20260524223045',
|
|
165
|
+
});
|
|
166
|
+
expect(a.targetAcId).toBeDefined();
|
|
167
|
+
expect(a.versionPin).toBe('20260524223045');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('rejects malformed targetTaskId via schema', () => {
|
|
171
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
172
|
+
kind: 'satisfies',
|
|
173
|
+
targetTaskId: 'task-1234',
|
|
174
|
+
targetAcAlias: 'AC2',
|
|
175
|
+
});
|
|
176
|
+
expect(r.success).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('rejects mixed-case UUID via schema', () => {
|
|
180
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
181
|
+
kind: 'satisfies',
|
|
182
|
+
targetTaskId: 'T10495',
|
|
183
|
+
targetAcId: '8F4A2C1E-B09D-4F6A-9C3E-7A1D4F8C0B2E',
|
|
184
|
+
});
|
|
185
|
+
expect(r.success).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('rejects malformed version-pin via schema', () => {
|
|
189
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
190
|
+
kind: 'satisfies',
|
|
191
|
+
targetTaskId: 'T10495',
|
|
192
|
+
targetAcAlias: 'AC2',
|
|
193
|
+
versionPin: '2026-05-24T22:30',
|
|
194
|
+
});
|
|
195
|
+
expect(r.success).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('round-trips through the EvidenceAtomSchema discriminated union', () => {
|
|
199
|
+
const a = EvidenceAtomSchema.parse({
|
|
200
|
+
kind: 'satisfies',
|
|
201
|
+
targetTaskId: 'T10495',
|
|
202
|
+
targetAcAlias: 'AC2',
|
|
203
|
+
});
|
|
204
|
+
expect(a.kind).toBe('satisfies');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── parseEvidenceString (CLI parser) ──────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
describe('parseEvidenceString — satisfies atom (T10506)', () => {
|
|
211
|
+
// ─── Valid forms ─────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
it('parses UUID form', () => {
|
|
214
|
+
const atoms = parseEvidenceString('satisfies:T10495#8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e');
|
|
215
|
+
expect(atoms).toHaveLength(1);
|
|
216
|
+
expect(atoms[0]).toEqual({
|
|
217
|
+
kind: 'satisfies',
|
|
218
|
+
targetTaskId: 'T10495',
|
|
219
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
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
|
+
|
|
233
|
+
it('parses alias form', () => {
|
|
234
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2');
|
|
235
|
+
expect(atoms).toHaveLength(1);
|
|
236
|
+
expect(atoms[0]).toEqual({
|
|
237
|
+
kind: 'satisfies',
|
|
238
|
+
targetTaskId: 'T10495',
|
|
239
|
+
targetAcAlias: 'AC2',
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('parses alias form with version-pin', () => {
|
|
244
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2@20260524223045');
|
|
245
|
+
expect(atoms).toHaveLength(1);
|
|
246
|
+
expect(atoms[0]).toEqual({
|
|
247
|
+
kind: 'satisfies',
|
|
248
|
+
targetTaskId: 'T10495',
|
|
249
|
+
targetAcAlias: 'AC2',
|
|
250
|
+
versionPin: '20260524223045',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('parses UUID form with version-pin (canonical highest-trust)', () => {
|
|
255
|
+
const atoms = parseEvidenceString(
|
|
256
|
+
'satisfies:T10495#8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e@20260524223045',
|
|
257
|
+
);
|
|
258
|
+
expect(atoms).toHaveLength(1);
|
|
259
|
+
expect(atoms[0]).toEqual({
|
|
260
|
+
kind: 'satisfies',
|
|
261
|
+
targetTaskId: 'T10495',
|
|
262
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
263
|
+
versionPin: '20260524223045',
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('parses multiple satisfies atoms in one evidence string', () => {
|
|
268
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2;satisfies:T10493#AC1');
|
|
269
|
+
expect(atoms).toHaveLength(2);
|
|
270
|
+
expect(atoms[0]).toMatchObject({
|
|
271
|
+
kind: 'satisfies',
|
|
272
|
+
targetTaskId: 'T10495',
|
|
273
|
+
targetAcAlias: 'AC2',
|
|
274
|
+
});
|
|
275
|
+
expect(atoms[1]).toMatchObject({
|
|
276
|
+
kind: 'satisfies',
|
|
277
|
+
targetTaskId: 'T10493',
|
|
278
|
+
targetAcAlias: 'AC1',
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('parses mixed evidence: commit + files + satisfies', () => {
|
|
283
|
+
const atoms = parseEvidenceString(
|
|
284
|
+
'commit:abc1234;files:src/foo.ts,src/bar.ts;satisfies:T10495#AC2',
|
|
285
|
+
);
|
|
286
|
+
expect(atoms).toHaveLength(3);
|
|
287
|
+
expect(atoms[0]).toEqual({ kind: 'commit', sha: 'abc1234' });
|
|
288
|
+
expect(atoms[1]).toEqual({ kind: 'files', paths: ['src/foo.ts', 'src/bar.ts'] });
|
|
289
|
+
expect(atoms[2]).toEqual({
|
|
290
|
+
kind: 'satisfies',
|
|
291
|
+
targetTaskId: 'T10495',
|
|
292
|
+
targetAcAlias: 'AC2',
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('parses mixed evidence: satisfies + pr (single PR landing multiple ACs)', () => {
|
|
297
|
+
const atoms = parseEvidenceString('satisfies:T10506#AC1;satisfies:T10506#AC2;pr:773');
|
|
298
|
+
expect(atoms).toHaveLength(3);
|
|
299
|
+
expect(atoms[2]).toEqual({ kind: 'pr', prNumber: 773 });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ─── Malformed: missing colon ────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
it('rejects atom missing the kind-payload colon', () => {
|
|
305
|
+
// "satisfiesT10495#AC2" — no `:` separator at all
|
|
306
|
+
expect(() => parseEvidenceString('satisfiesT10495#AC2')).toThrow(EvidenceParseError);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ─── Malformed: missing hash ─────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
it('rejects atom missing the # separator between task-id and ac-id', () => {
|
|
312
|
+
expect(() => parseEvidenceString('satisfies:T10495-AC2')).toThrow(EvidenceParseError);
|
|
313
|
+
expect(() => parseEvidenceString('satisfies:T10495AC2')).toThrow(EvidenceParseError);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('rejects atom with # at start of payload (empty task-id)', () => {
|
|
317
|
+
expect(() => parseEvidenceString('satisfies:#AC2')).toThrow(EvidenceParseError);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('rejects atom with # at end of payload (empty ac-id)', () => {
|
|
321
|
+
expect(() => parseEvidenceString('satisfies:T10495#')).toThrow(EvidenceParseError);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ─── Malformed: missing T-prefix on task-id ──────────────────────────────
|
|
325
|
+
|
|
326
|
+
it('rejects atom with non-T-prefixed task-id', () => {
|
|
327
|
+
expect(() => parseEvidenceString('satisfies:10495#AC2')).toThrow(EvidenceParseError);
|
|
328
|
+
expect(() => parseEvidenceString('satisfies:task-10495#AC2')).toThrow(EvidenceParseError);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('rejects atom with lowercase t prefix', () => {
|
|
332
|
+
expect(() => parseEvidenceString('satisfies:t10495#AC2')).toThrow(EvidenceParseError);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('rejects atom with task-id exceeding 7 digits', () => {
|
|
336
|
+
expect(() => parseEvidenceString('satisfies:T12345678#AC2')).toThrow(EvidenceParseError);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ─── Malformed: ac-id ────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
it('rejects atom with mixed-case UUID', () => {
|
|
342
|
+
// Council §2.2 strictness — silent dedupe prevention
|
|
343
|
+
expect(() =>
|
|
344
|
+
parseEvidenceString('satisfies:T10495#8F4A2C1E-B09D-4F6A-9C3E-7A1D4F8C0B2E'),
|
|
345
|
+
).toThrow(EvidenceParseError);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('rejects atom with ac-id that is neither UUID nor AC<digits> alias', () => {
|
|
349
|
+
expect(() => parseEvidenceString('satisfies:T10495#hello')).toThrow(EvidenceParseError);
|
|
350
|
+
expect(() => parseEvidenceString('satisfies:T10495#2')).toThrow(EvidenceParseError);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('rejects atom with alias exceeding 4 digits', () => {
|
|
354
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC10000')).toThrow(EvidenceParseError);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ─── Malformed: version-pin ──────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
it('rejects atom with malformed version-pin (not 14 digits)', () => {
|
|
360
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@2026-05-24')).toThrow(
|
|
361
|
+
EvidenceParseError,
|
|
362
|
+
);
|
|
363
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@2026052422304')).toThrow(
|
|
364
|
+
EvidenceParseError,
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('rejects atom with @ but empty version-pin payload', () => {
|
|
369
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@')).toThrow(EvidenceParseError);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ─── Error message smoke: ensures kind name appears in default-case ──────
|
|
373
|
+
|
|
374
|
+
it('includes "satisfies" in the unknown-kind error suggestion list', () => {
|
|
375
|
+
try {
|
|
376
|
+
parseEvidenceString('unknownkind:foo');
|
|
377
|
+
throw new Error('expected EvidenceParseError');
|
|
378
|
+
} catch (err) {
|
|
379
|
+
expect(err).toBeInstanceOf(EvidenceParseError);
|
|
380
|
+
expect((err as EvidenceParseError).fix).toContain('satisfies');
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|