@cleocode/contracts 2026.5.111 → 2026.5.113
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__/cli-meta-suggested-next.test.d.ts +21 -0
- package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts +16 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.js +394 -0
- package/dist/__tests__/evidence-atom-schema.test.js.map +1 -0
- package/dist/config/__tests__/manifest.test.d.ts +13 -0
- package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/config/__tests__/manifest.test.js +143 -0
- package/dist/config/__tests__/manifest.test.js.map +1 -0
- package/dist/config/manifest.d.ts +158 -0
- package/dist/config/manifest.d.ts.map +1 -0
- package/dist/config/manifest.js +123 -0
- package/dist/config/manifest.js.map +1 -0
- package/dist/docs/__tests__/provenance.test.d.ts +14 -0
- package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
- package/dist/docs/__tests__/provenance.test.js +579 -0
- package/dist/docs/__tests__/provenance.test.js.map +1 -0
- package/dist/docs/provenance.d.ts +731 -0
- package/dist/docs/provenance.d.ts.map +1 -0
- package/dist/docs/provenance.js +258 -0
- package/dist/docs/provenance.js.map +1 -0
- package/dist/docs-taxonomy.d.ts +24 -0
- package/dist/docs-taxonomy.d.ts.map +1 -1
- package/dist/docs-taxonomy.js +27 -0
- package/dist/docs-taxonomy.js.map +1 -1
- package/dist/doctor.d.ts +101 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +407 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -0
- package/dist/evidence-atom-schema.js +505 -0
- package/dist/evidence-atom-schema.js.map +1 -0
- package/dist/exit-codes.d.ts +37 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +37 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
- package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
- package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
- package/dist/invariants/__tests__/registry.test.d.ts +14 -0
- package/dist/invariants/__tests__/registry.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/registry.test.js +113 -0
- package/dist/invariants/__tests__/registry.test.js.map +1 -0
- package/dist/invariants/adr-056-release.d.ts +42 -0
- package/dist/invariants/adr-056-release.d.ts.map +1 -0
- package/dist/invariants/adr-056-release.js +143 -0
- package/dist/invariants/adr-056-release.js.map +1 -0
- package/dist/invariants/adr-070-orchestration.d.ts +73 -0
- package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
- package/dist/invariants/adr-070-orchestration.js +290 -0
- package/dist/invariants/adr-070-orchestration.js.map +1 -0
- package/dist/invariants/adr-073-saga.d.ts +32 -0
- package/dist/invariants/adr-073-saga.d.ts.map +1 -0
- package/dist/invariants/adr-073-saga.js +155 -0
- package/dist/invariants/adr-073-saga.js.map +1 -0
- package/dist/invariants/index.d.ts +162 -0
- package/dist/invariants/index.d.ts.map +1 -0
- package/dist/invariants/index.js +118 -0
- package/dist/invariants/index.js.map +1 -0
- package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
- package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/input-contract.test.js +158 -0
- package/dist/operations/__tests__/input-contract.test.js.map +1 -0
- package/dist/operations/docs.d.ts +16 -0
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/input-contract.d.ts +267 -0
- package/dist/operations/input-contract.d.ts.map +1 -0
- package/dist/operations/input-contract.js +29 -0
- package/dist/operations/input-contract.js.map +1 -0
- package/dist/templates/__tests__/manifest.test.d.ts +18 -0
- package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/templates/__tests__/manifest.test.js +123 -0
- package/dist/templates/__tests__/manifest.test.js.map +1 -0
- package/dist/templates/manifest.d.ts +186 -0
- package/dist/templates/manifest.d.ts.map +1 -0
- package/dist/templates/manifest.js +165 -0
- package/dist/templates/manifest.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
- package/src/__tests__/evidence-atom-schema.test.ts +499 -0
- package/src/config/__tests__/manifest.test.ts +176 -0
- package/src/config/manifest.ts +218 -0
- package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
- package/src/docs/__tests__/provenance.test.ts +648 -0
- package/src/docs/provenance.ts +601 -0
- package/src/docs-taxonomy.ts +55 -0
- package/src/doctor.ts +111 -0
- package/src/errors.ts +4 -0
- package/src/evidence-atom-schema.ts +628 -0
- package/src/exit-codes.ts +39 -0
- package/src/index.ts +129 -0
- package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
- package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
- package/src/invariants/__tests__/registry.test.ts +136 -0
- package/src/invariants/adr-056-release.ts +154 -0
- package/src/invariants/adr-070-orchestration.ts +317 -0
- package/src/invariants/adr-073-saga.ts +167 -0
- package/src/invariants/index.ts +222 -0
- package/src/operations/__tests__/input-contract.test.ts +188 -0
- package/src/operations/docs.ts +16 -0
- package/src/operations/input-contract.ts +301 -0
- package/src/templates/__tests__/manifest.test.ts +140 -0
- package/src/templates/manifest.ts +208 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ADR-051 evidence atom grammar schema (T10337).
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - Per-atom-prefix valid + invalid parse cases.
|
|
6
|
+
* - Per-gate satisfying + insufficient atom sets.
|
|
7
|
+
* - Round-trip: parseEvidenceString output matches schema parse.
|
|
8
|
+
* - Behavior parity with the legacy `checkGateEvidenceMinimum` error
|
|
9
|
+
* message format.
|
|
10
|
+
*
|
|
11
|
+
* @task T10337
|
|
12
|
+
* @saga T10326
|
|
13
|
+
* @adr ADR-051
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
import {
|
|
18
|
+
type EvidenceAtomInput,
|
|
19
|
+
EvidenceAtomSchema,
|
|
20
|
+
EvidenceParseError,
|
|
21
|
+
formatGateRequirement,
|
|
22
|
+
GATE_EVIDENCE_REQUIREMENTS,
|
|
23
|
+
parseEvidenceString,
|
|
24
|
+
validateEvidenceForGate,
|
|
25
|
+
} from '../index.js';
|
|
26
|
+
import type { VerificationGate } from '../task.js';
|
|
27
|
+
|
|
28
|
+
describe('EvidenceAtomSchema (T10337)', () => {
|
|
29
|
+
// ─── Per-atom-prefix parse coverage ───────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe('commit atom', () => {
|
|
32
|
+
it('accepts 7-40 hex SHA', () => {
|
|
33
|
+
const a = EvidenceAtomSchema.parse({ kind: 'commit', sha: 'abc1234' });
|
|
34
|
+
expect(a).toEqual({ kind: 'commit', sha: 'abc1234' });
|
|
35
|
+
const b = EvidenceAtomSchema.parse({
|
|
36
|
+
kind: 'commit',
|
|
37
|
+
sha: 'a'.repeat(40),
|
|
38
|
+
});
|
|
39
|
+
expect(b.kind).toBe('commit');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects short SHA', () => {
|
|
43
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'commit', sha: 'abc' });
|
|
44
|
+
expect(r.success).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects non-hex SHA', () => {
|
|
48
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'commit', sha: 'xyz1234' });
|
|
49
|
+
expect(r.success).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('files atom', () => {
|
|
54
|
+
it('accepts non-empty paths array', () => {
|
|
55
|
+
const a = EvidenceAtomSchema.parse({
|
|
56
|
+
kind: 'files',
|
|
57
|
+
paths: ['src/foo.ts', 'src/bar.ts'],
|
|
58
|
+
});
|
|
59
|
+
expect(a).toEqual({ kind: 'files', paths: ['src/foo.ts', 'src/bar.ts'] });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects empty paths array', () => {
|
|
63
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'files', paths: [] });
|
|
64
|
+
expect(r.success).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('test-run atom', () => {
|
|
69
|
+
it('accepts non-empty path', () => {
|
|
70
|
+
const a = EvidenceAtomSchema.parse({ kind: 'test-run', path: '/tmp/v.json' });
|
|
71
|
+
expect(a).toEqual({ kind: 'test-run', path: '/tmp/v.json' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects empty path', () => {
|
|
75
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'test-run', path: '' });
|
|
76
|
+
expect(r.success).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('tool atom', () => {
|
|
81
|
+
it('accepts canonical and alias names', () => {
|
|
82
|
+
expect(EvidenceAtomSchema.parse({ kind: 'tool', tool: 'test' }).kind).toBe('tool');
|
|
83
|
+
expect(EvidenceAtomSchema.parse({ kind: 'tool', tool: 'pnpm-test' }).kind).toBe('tool');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects empty tool name', () => {
|
|
87
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'tool', tool: '' });
|
|
88
|
+
expect(r.success).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('url atom', () => {
|
|
93
|
+
it('accepts http and https', () => {
|
|
94
|
+
expect(EvidenceAtomSchema.parse({ kind: 'url', url: 'https://example.com/x' }).kind).toBe(
|
|
95
|
+
'url',
|
|
96
|
+
);
|
|
97
|
+
expect(EvidenceAtomSchema.parse({ kind: 'url', url: 'http://localhost:8080' }).kind).toBe(
|
|
98
|
+
'url',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects scheme-less url', () => {
|
|
103
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'url', url: 'example.com' });
|
|
104
|
+
expect(r.success).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('note atom', () => {
|
|
109
|
+
it('accepts 1-512 chars', () => {
|
|
110
|
+
expect(EvidenceAtomSchema.parse({ kind: 'note', note: 'x' }).kind).toBe('note');
|
|
111
|
+
expect(EvidenceAtomSchema.parse({ kind: 'note', note: 'a'.repeat(512) }).kind).toBe('note');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('rejects empty and >512 char notes', () => {
|
|
115
|
+
expect(EvidenceAtomSchema.safeParse({ kind: 'note', note: '' }).success).toBe(false);
|
|
116
|
+
expect(EvidenceAtomSchema.safeParse({ kind: 'note', note: 'a'.repeat(513) }).success).toBe(
|
|
117
|
+
false,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('decision atom', () => {
|
|
123
|
+
it('accepts non-empty decision ID', () => {
|
|
124
|
+
const a = EvidenceAtomSchema.parse({ kind: 'decision', decisionId: 'D-arch-001' });
|
|
125
|
+
expect(a).toEqual({ kind: 'decision', decisionId: 'D-arch-001' });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects empty decision ID', () => {
|
|
129
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'decision', decisionId: '' });
|
|
130
|
+
expect(r.success).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('pr atom', () => {
|
|
135
|
+
it('accepts positive integer', () => {
|
|
136
|
+
const a = EvidenceAtomSchema.parse({ kind: 'pr', prNumber: 357 });
|
|
137
|
+
expect(a).toEqual({ kind: 'pr', prNumber: 357 });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rejects zero, negatives, and non-integers', () => {
|
|
141
|
+
expect(EvidenceAtomSchema.safeParse({ kind: 'pr', prNumber: 0 }).success).toBe(false);
|
|
142
|
+
expect(EvidenceAtomSchema.safeParse({ kind: 'pr', prNumber: -1 }).success).toBe(false);
|
|
143
|
+
expect(EvidenceAtomSchema.safeParse({ kind: 'pr', prNumber: 1.5 }).success).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('loc-drop atom', () => {
|
|
148
|
+
it('accepts non-negative integers', () => {
|
|
149
|
+
const a = EvidenceAtomSchema.parse({
|
|
150
|
+
kind: 'loc-drop',
|
|
151
|
+
fromLines: 1200,
|
|
152
|
+
toLines: 800,
|
|
153
|
+
});
|
|
154
|
+
expect(a.kind).toBe('loc-drop');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rejects negative line counts', () => {
|
|
158
|
+
const r = EvidenceAtomSchema.safeParse({
|
|
159
|
+
kind: 'loc-drop',
|
|
160
|
+
fromLines: -1,
|
|
161
|
+
toLines: 0,
|
|
162
|
+
});
|
|
163
|
+
expect(r.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('callsite-coverage atom', () => {
|
|
168
|
+
it('accepts non-empty symbol + path', () => {
|
|
169
|
+
const a = EvidenceAtomSchema.parse({
|
|
170
|
+
kind: 'callsite-coverage',
|
|
171
|
+
symbolName: 'myFn',
|
|
172
|
+
relativeSourcePath: 'packages/core/src/myFn.ts',
|
|
173
|
+
});
|
|
174
|
+
expect(a.kind).toBe('callsite-coverage');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('rejects empty symbol or path', () => {
|
|
178
|
+
expect(
|
|
179
|
+
EvidenceAtomSchema.safeParse({
|
|
180
|
+
kind: 'callsite-coverage',
|
|
181
|
+
symbolName: '',
|
|
182
|
+
relativeSourcePath: 'x',
|
|
183
|
+
}).success,
|
|
184
|
+
).toBe(false);
|
|
185
|
+
expect(
|
|
186
|
+
EvidenceAtomSchema.safeParse({
|
|
187
|
+
kind: 'callsite-coverage',
|
|
188
|
+
symbolName: 'x',
|
|
189
|
+
relativeSourcePath: '',
|
|
190
|
+
}).success,
|
|
191
|
+
).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('rejects unknown atom kind', () => {
|
|
196
|
+
const r = EvidenceAtomSchema.safeParse({ kind: 'mystery', payload: 'x' });
|
|
197
|
+
expect(r.success).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── parseEvidenceString — string → atom[] ─────────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe('parseEvidenceString (T10337)', () => {
|
|
204
|
+
it('parses a single commit atom', () => {
|
|
205
|
+
expect(parseEvidenceString('commit:abc1234')).toEqual([{ kind: 'commit', sha: 'abc1234' }]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('parses files with comma-separated paths', () => {
|
|
209
|
+
expect(parseEvidenceString('files:a.ts,b.ts,c.ts')).toEqual([
|
|
210
|
+
{ kind: 'files', paths: ['a.ts', 'b.ts', 'c.ts'] },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('parses test-run, tool, url, note atoms', () => {
|
|
215
|
+
expect(parseEvidenceString('test-run:/tmp/v.json')).toEqual([
|
|
216
|
+
{ kind: 'test-run', path: '/tmp/v.json' },
|
|
217
|
+
]);
|
|
218
|
+
expect(parseEvidenceString('tool:lint')).toEqual([{ kind: 'tool', tool: 'lint' }]);
|
|
219
|
+
expect(parseEvidenceString('url:https://example.com')).toEqual([
|
|
220
|
+
{ kind: 'url', url: 'https://example.com' },
|
|
221
|
+
]);
|
|
222
|
+
expect(parseEvidenceString('note:no network surface')).toEqual([
|
|
223
|
+
{ kind: 'note', note: 'no network surface' },
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('parses decision and pr atoms', () => {
|
|
228
|
+
expect(parseEvidenceString('decision:D-arch-001')).toEqual([
|
|
229
|
+
{ kind: 'decision', decisionId: 'D-arch-001' },
|
|
230
|
+
]);
|
|
231
|
+
expect(parseEvidenceString('pr:357')).toEqual([{ kind: 'pr', prNumber: 357 }]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('parses loc-drop and callsite-coverage atoms', () => {
|
|
235
|
+
expect(parseEvidenceString('loc-drop:1200:800')).toEqual([
|
|
236
|
+
{ kind: 'loc-drop', fromLines: 1200, toLines: 800 },
|
|
237
|
+
]);
|
|
238
|
+
expect(parseEvidenceString('callsite-coverage:myFn:packages/core/src/myFn.ts')).toEqual([
|
|
239
|
+
{
|
|
240
|
+
kind: 'callsite-coverage',
|
|
241
|
+
symbolName: 'myFn',
|
|
242
|
+
relativeSourcePath: 'packages/core/src/myFn.ts',
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('parses multi-atom evidence strings with semicolons', () => {
|
|
248
|
+
const atoms = parseEvidenceString('commit:abc1234;files:src/a.ts,src/b.ts;tool:lint');
|
|
249
|
+
expect(atoms).toEqual([
|
|
250
|
+
{ kind: 'commit', sha: 'abc1234' },
|
|
251
|
+
{ kind: 'files', paths: ['src/a.ts', 'src/b.ts'] },
|
|
252
|
+
{ kind: 'tool', tool: 'lint' },
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('tolerates whitespace around separators', () => {
|
|
257
|
+
expect(parseEvidenceString(' commit:abc1234 ; tool:test ')).toEqual([
|
|
258
|
+
{ kind: 'commit', sha: 'abc1234' },
|
|
259
|
+
{ kind: 'tool', tool: 'test' },
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('consumes state:MERGED modifier in-place (no separate atom)', () => {
|
|
264
|
+
expect(parseEvidenceString('pr:357;state:MERGED')).toEqual([{ kind: 'pr', prNumber: 357 }]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('rejects state:MERGED without preceding pr atom', () => {
|
|
268
|
+
expect(() => parseEvidenceString('state:MERGED')).toThrow(EvidenceParseError);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('rejects state with non-MERGED value', () => {
|
|
272
|
+
expect(() => parseEvidenceString('pr:357;state:OPEN')).toThrow(EvidenceParseError);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('rejects empty evidence string', () => {
|
|
276
|
+
expect(() => parseEvidenceString('')).toThrow(EvidenceParseError);
|
|
277
|
+
expect(() => parseEvidenceString(';;;')).toThrow(EvidenceParseError);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('rejects malformed atoms', () => {
|
|
281
|
+
expect(() => parseEvidenceString('justakind')).toThrow(EvidenceParseError);
|
|
282
|
+
expect(() => parseEvidenceString(':payloadonly')).toThrow(EvidenceParseError);
|
|
283
|
+
expect(() => parseEvidenceString('kind:')).toThrow(EvidenceParseError);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('rejects unknown kinds', () => {
|
|
287
|
+
expect(() => parseEvidenceString('mystery:foo')).toThrow(EvidenceParseError);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('rejects malformed loc-drop', () => {
|
|
291
|
+
expect(() => parseEvidenceString('loc-drop:notanumber:0')).toThrow(EvidenceParseError);
|
|
292
|
+
expect(() => parseEvidenceString('loc-drop:1200')).toThrow(EvidenceParseError);
|
|
293
|
+
expect(() => parseEvidenceString('loc-drop:-1:0')).toThrow(EvidenceParseError);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('rejects malformed pr', () => {
|
|
297
|
+
expect(() => parseEvidenceString('pr:notanumber')).toThrow(EvidenceParseError);
|
|
298
|
+
expect(() => parseEvidenceString('pr:0')).toThrow(EvidenceParseError);
|
|
299
|
+
expect(() => parseEvidenceString('pr:-5')).toThrow(EvidenceParseError);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('rejects malformed callsite-coverage', () => {
|
|
303
|
+
expect(() => parseEvidenceString('callsite-coverage:fn')).toThrow(EvidenceParseError);
|
|
304
|
+
expect(() => parseEvidenceString('callsite-coverage::p.ts')).toThrow(EvidenceParseError);
|
|
305
|
+
expect(() => parseEvidenceString('callsite-coverage:fn:')).toThrow(EvidenceParseError);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('round-trip: every parseEvidenceString output validates under EvidenceAtomSchema', () => {
|
|
309
|
+
const inputs = [
|
|
310
|
+
'commit:abc1234',
|
|
311
|
+
'files:a.ts,b.ts',
|
|
312
|
+
'test-run:/tmp/x.json',
|
|
313
|
+
'tool:lint',
|
|
314
|
+
'url:https://example.com',
|
|
315
|
+
'note:waiver text',
|
|
316
|
+
'decision:D-arch-001',
|
|
317
|
+
'pr:357',
|
|
318
|
+
'loc-drop:1200:800',
|
|
319
|
+
'callsite-coverage:fn:src/fn.ts',
|
|
320
|
+
];
|
|
321
|
+
for (const raw of inputs) {
|
|
322
|
+
const atoms = parseEvidenceString(raw);
|
|
323
|
+
for (const atom of atoms) {
|
|
324
|
+
const r = EvidenceAtomSchema.safeParse(atom);
|
|
325
|
+
expect(r.success, `${raw} -> ${JSON.stringify(atom)}`).toBe(true);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ─── GATE_EVIDENCE_REQUIREMENTS — gate-to-atom mapping ─────────────────────
|
|
332
|
+
|
|
333
|
+
describe('GATE_EVIDENCE_REQUIREMENTS (T10337)', () => {
|
|
334
|
+
it('covers every VerificationGate', () => {
|
|
335
|
+
const expected: VerificationGate[] = [
|
|
336
|
+
'implemented',
|
|
337
|
+
'testsPassed',
|
|
338
|
+
'qaPassed',
|
|
339
|
+
'documented',
|
|
340
|
+
'securityPassed',
|
|
341
|
+
'cleanupDone',
|
|
342
|
+
'nexusImpact',
|
|
343
|
+
];
|
|
344
|
+
for (const g of expected) {
|
|
345
|
+
expect(GATE_EVIDENCE_REQUIREMENTS[g]).toBeDefined();
|
|
346
|
+
expect(GATE_EVIDENCE_REQUIREMENTS[g].oneOf.length).toBeGreaterThan(0);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('implemented accepts the 5 canonical combinations', () => {
|
|
351
|
+
const a: EvidenceAtomInput[] = [
|
|
352
|
+
{ kind: 'commit', sha: 'abc1234' },
|
|
353
|
+
{ kind: 'files', paths: ['x.ts'] },
|
|
354
|
+
];
|
|
355
|
+
expect(validateEvidenceForGate('implemented', a).ok).toBe(true);
|
|
356
|
+
|
|
357
|
+
const b: EvidenceAtomInput[] = [
|
|
358
|
+
{ kind: 'commit', sha: 'abc1234' },
|
|
359
|
+
{ kind: 'note', note: 'deleted x.ts' },
|
|
360
|
+
];
|
|
361
|
+
expect(validateEvidenceForGate('implemented', b).ok).toBe(true);
|
|
362
|
+
|
|
363
|
+
const c: EvidenceAtomInput[] = [
|
|
364
|
+
{ kind: 'decision', decisionId: 'D-1' },
|
|
365
|
+
{ kind: 'files', paths: ['research.md'] },
|
|
366
|
+
];
|
|
367
|
+
expect(validateEvidenceForGate('implemented', c).ok).toBe(true);
|
|
368
|
+
|
|
369
|
+
const d: EvidenceAtomInput[] = [
|
|
370
|
+
{ kind: 'decision', decisionId: 'D-1' },
|
|
371
|
+
{ kind: 'note', note: 'decision-only' },
|
|
372
|
+
];
|
|
373
|
+
expect(validateEvidenceForGate('implemented', d).ok).toBe(true);
|
|
374
|
+
|
|
375
|
+
const e: EvidenceAtomInput[] = [{ kind: 'pr', prNumber: 357 }];
|
|
376
|
+
expect(validateEvidenceForGate('implemented', e).ok).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('implemented rejects insufficient sets with the legacy error format', () => {
|
|
380
|
+
const r = validateEvidenceForGate('implemented', [
|
|
381
|
+
{ kind: 'commit', sha: 'abc1234' },
|
|
382
|
+
] as EvidenceAtomInput[]);
|
|
383
|
+
expect(r.ok).toBe(false);
|
|
384
|
+
if (!r.ok) {
|
|
385
|
+
expect(r.message).toMatch(/^Gate 'implemented' requires evidence: /);
|
|
386
|
+
expect(r.message).toContain('[commit AND files]');
|
|
387
|
+
expect(r.message).toContain('[pr]');
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('testsPassed accepts test-run, tool, pr — single-atom alternatives', () => {
|
|
392
|
+
expect(
|
|
393
|
+
validateEvidenceForGate('testsPassed', [
|
|
394
|
+
{ kind: 'test-run', path: '/tmp/v.json' },
|
|
395
|
+
] as EvidenceAtomInput[]).ok,
|
|
396
|
+
).toBe(true);
|
|
397
|
+
expect(
|
|
398
|
+
validateEvidenceForGate('testsPassed', [
|
|
399
|
+
{ kind: 'tool', tool: 'test' },
|
|
400
|
+
] as EvidenceAtomInput[]).ok,
|
|
401
|
+
).toBe(true);
|
|
402
|
+
expect(
|
|
403
|
+
validateEvidenceForGate('testsPassed', [{ kind: 'pr', prNumber: 357 }] as EvidenceAtomInput[])
|
|
404
|
+
.ok,
|
|
405
|
+
).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('testsPassed rejects empty evidence', () => {
|
|
409
|
+
expect(validateEvidenceForGate('testsPassed', []).ok).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('qaPassed accepts a single tool atom (behavior parity with legacy)', () => {
|
|
413
|
+
// Critical parity check: GATE_EVIDENCE_REQUIREMENTS.qaPassed.oneOf is
|
|
414
|
+
// [['tool'], ['pr']] — not [['tool', 'tool']]. The legacy runtime did NOT
|
|
415
|
+
// count distinct tool names, so one tool atom passes.
|
|
416
|
+
expect(
|
|
417
|
+
validateEvidenceForGate('qaPassed', [{ kind: 'tool', tool: 'lint' }] as EvidenceAtomInput[])
|
|
418
|
+
.ok,
|
|
419
|
+
).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('qaPassed also accepts pr atom', () => {
|
|
423
|
+
expect(
|
|
424
|
+
validateEvidenceForGate('qaPassed', [{ kind: 'pr', prNumber: 357 }] as EvidenceAtomInput[])
|
|
425
|
+
.ok,
|
|
426
|
+
).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('documented accepts files or url', () => {
|
|
430
|
+
expect(
|
|
431
|
+
validateEvidenceForGate('documented', [
|
|
432
|
+
{ kind: 'files', paths: ['docs/x.md'] },
|
|
433
|
+
] as EvidenceAtomInput[]).ok,
|
|
434
|
+
).toBe(true);
|
|
435
|
+
expect(
|
|
436
|
+
validateEvidenceForGate('documented', [
|
|
437
|
+
{ kind: 'url', url: 'https://docs.example.com' },
|
|
438
|
+
] as EvidenceAtomInput[]).ok,
|
|
439
|
+
).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('securityPassed accepts tool or note waiver', () => {
|
|
443
|
+
expect(
|
|
444
|
+
validateEvidenceForGate('securityPassed', [
|
|
445
|
+
{ kind: 'tool', tool: 'security-scan' },
|
|
446
|
+
] as EvidenceAtomInput[]).ok,
|
|
447
|
+
).toBe(true);
|
|
448
|
+
expect(
|
|
449
|
+
validateEvidenceForGate('securityPassed', [
|
|
450
|
+
{ kind: 'note', note: 'no network surface' },
|
|
451
|
+
] as EvidenceAtomInput[]).ok,
|
|
452
|
+
).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('cleanupDone accepts only note', () => {
|
|
456
|
+
expect(
|
|
457
|
+
validateEvidenceForGate('cleanupDone', [
|
|
458
|
+
{ kind: 'note', note: 'removed dead branches' },
|
|
459
|
+
] as EvidenceAtomInput[]).ok,
|
|
460
|
+
).toBe(true);
|
|
461
|
+
expect(
|
|
462
|
+
validateEvidenceForGate('cleanupDone', [
|
|
463
|
+
{ kind: 'tool', tool: 'test' },
|
|
464
|
+
] as EvidenceAtomInput[]).ok,
|
|
465
|
+
).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('nexusImpact accepts tool or note waiver', () => {
|
|
469
|
+
expect(
|
|
470
|
+
validateEvidenceForGate('nexusImpact', [
|
|
471
|
+
{ kind: 'tool', tool: 'nexus-impact-full' },
|
|
472
|
+
] as EvidenceAtomInput[]).ok,
|
|
473
|
+
).toBe(true);
|
|
474
|
+
expect(
|
|
475
|
+
validateEvidenceForGate('nexusImpact', [
|
|
476
|
+
{ kind: 'note', note: 'nexus gate disabled' },
|
|
477
|
+
] as EvidenceAtomInput[]).ok,
|
|
478
|
+
).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ─── formatGateRequirement — error message helper ──────────────────────────
|
|
483
|
+
|
|
484
|
+
describe('formatGateRequirement (T10337)', () => {
|
|
485
|
+
it('formats implemented combinations as legacy string', () => {
|
|
486
|
+
const s = formatGateRequirement('implemented');
|
|
487
|
+
expect(s).toContain('[commit AND files]');
|
|
488
|
+
expect(s).toContain('[commit AND note]');
|
|
489
|
+
expect(s).toContain('[decision AND files]');
|
|
490
|
+
expect(s).toContain('[decision AND note]');
|
|
491
|
+
expect(s).toContain('[pr]');
|
|
492
|
+
// Joiner between combinations is ' OR '.
|
|
493
|
+
expect(s.split(' OR ').length).toBe(5);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('formats single-element combination without AND', () => {
|
|
497
|
+
expect(formatGateRequirement('cleanupDone')).toBe('[note]');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ConfigManifest contract (T9876 / Saga T9855 / ADR-076).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Schema round-trip for each of the 4 built-in entries.
|
|
6
|
+
* - Merge-precedence invariant (project > global > defaults).
|
|
7
|
+
* - Metadata-scope isolation (NOT in the merge chain).
|
|
8
|
+
* - {@link DriftDetection} exhaustiveness via a `never` fallback switch.
|
|
9
|
+
*
|
|
10
|
+
* @task T9876
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
CLEO_CONFIG_MANIFEST,
|
|
16
|
+
CONFIG_MANIFEST_ENTRIES,
|
|
17
|
+
type ConfigManifestEntry,
|
|
18
|
+
type ConfigScope,
|
|
19
|
+
configManifestEntrySchema,
|
|
20
|
+
type DriftDetection,
|
|
21
|
+
GLOBAL_CLEO_CONFIG_MANIFEST,
|
|
22
|
+
PROJECT_CONTEXT_MANIFEST,
|
|
23
|
+
PROJECT_INFO_MANIFEST,
|
|
24
|
+
} from '../manifest.js';
|
|
25
|
+
|
|
26
|
+
describe('ConfigManifest built-in entries', () => {
|
|
27
|
+
const builtins: ReadonlyArray<readonly [string, ConfigManifestEntry]> = [
|
|
28
|
+
['PROJECT_INFO_MANIFEST', PROJECT_INFO_MANIFEST],
|
|
29
|
+
['PROJECT_CONTEXT_MANIFEST', PROJECT_CONTEXT_MANIFEST],
|
|
30
|
+
['CLEO_CONFIG_MANIFEST', CLEO_CONFIG_MANIFEST],
|
|
31
|
+
['GLOBAL_CLEO_CONFIG_MANIFEST', GLOBAL_CLEO_CONFIG_MANIFEST],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const [name, entry] of builtins) {
|
|
35
|
+
it(`${name} validates against configManifestEntrySchema`, () => {
|
|
36
|
+
const parsed = configManifestEntrySchema.parse(entry);
|
|
37
|
+
expect(parsed.id).toBe(entry.id);
|
|
38
|
+
expect(parsed.scope).toBe(entry.scope);
|
|
39
|
+
expect(parsed.path).toBe(entry.path);
|
|
40
|
+
expect(parsed.mergePrecedence).toBe(entry.mergePrecedence);
|
|
41
|
+
expect(parsed.driftDetection).toBe(entry.driftDetection);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('CONFIG_MANIFEST_ENTRIES contains all 4 built-in entries', () => {
|
|
46
|
+
const ids = CONFIG_MANIFEST_ENTRIES.map((e) => e.id).sort();
|
|
47
|
+
expect(ids).toEqual([
|
|
48
|
+
'cleo-config-global',
|
|
49
|
+
'cleo-config-project',
|
|
50
|
+
'project-context',
|
|
51
|
+
'project-info',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('CONFIG_MANIFEST_ENTRIES is frozen', () => {
|
|
56
|
+
expect(Object.isFrozen(CONFIG_MANIFEST_ENTRIES)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Merge-precedence invariant (project > global > defaults)', () => {
|
|
61
|
+
it('project precedence > global precedence (higher wins)', () => {
|
|
62
|
+
expect(CLEO_CONFIG_MANIFEST.mergePrecedence).toBeGreaterThan(
|
|
63
|
+
GLOBAL_CLEO_CONFIG_MANIFEST.mergePrecedence,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('canonical precedence values: project=20, global=10, metadata=0', () => {
|
|
68
|
+
expect(CLEO_CONFIG_MANIFEST.mergePrecedence).toBe(20);
|
|
69
|
+
expect(GLOBAL_CLEO_CONFIG_MANIFEST.mergePrecedence).toBe(10);
|
|
70
|
+
expect(PROJECT_INFO_MANIFEST.mergePrecedence).toBe(0);
|
|
71
|
+
expect(PROJECT_CONTEXT_MANIFEST.mergePrecedence).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('cascade-resolver sort order yields global then project (project wins)', () => {
|
|
75
|
+
const cascadeEntries = CONFIG_MANIFEST_ENTRIES.filter(
|
|
76
|
+
(e): e is ConfigManifestEntry & { scope: ConfigScope } =>
|
|
77
|
+
e.scope === 'global' || e.scope === 'project',
|
|
78
|
+
)
|
|
79
|
+
.slice() // copy before sort — array is frozen
|
|
80
|
+
.sort((a, b) => a.mergePrecedence - b.mergePrecedence);
|
|
81
|
+
|
|
82
|
+
expect(cascadeEntries.map((e) => e.scope)).toEqual(['global', 'project']);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Metadata-scope isolation', () => {
|
|
87
|
+
it('metadata entries are NOT in the cascade merge chain', () => {
|
|
88
|
+
const cascadeScopes = CONFIG_MANIFEST_ENTRIES.filter((e) => e.scope !== 'metadata').map(
|
|
89
|
+
(e) => e.scope,
|
|
90
|
+
);
|
|
91
|
+
expect(cascadeScopes).toEqual(expect.arrayContaining(['global', 'project']));
|
|
92
|
+
expect(cascadeScopes).not.toContain('metadata');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('all metadata entries have mergePrecedence 0 (separate channel)', () => {
|
|
96
|
+
const metadataEntries = CONFIG_MANIFEST_ENTRIES.filter((e) => e.scope === 'metadata');
|
|
97
|
+
expect(metadataEntries.length).toBeGreaterThan(0);
|
|
98
|
+
for (const e of metadataEntries) {
|
|
99
|
+
expect(e.mergePrecedence).toBe(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('DriftDetection exhaustiveness (type-level)', () => {
|
|
105
|
+
it('switch over DriftDetection covers all variants with never fallback', () => {
|
|
106
|
+
const describe_ = (d: DriftDetection): string => {
|
|
107
|
+
switch (d) {
|
|
108
|
+
case 'schema-validate':
|
|
109
|
+
return 'schema-validate';
|
|
110
|
+
case 'staleness-gate':
|
|
111
|
+
return 'staleness-gate';
|
|
112
|
+
case 'value-diff':
|
|
113
|
+
return 'value-diff';
|
|
114
|
+
case 'none':
|
|
115
|
+
return 'none';
|
|
116
|
+
default: {
|
|
117
|
+
// If a new variant is added without updating this switch, TS will
|
|
118
|
+
// refuse to assign it to `never` at compile time.
|
|
119
|
+
const _exhaustive: never = d;
|
|
120
|
+
return _exhaustive;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect(describe_('schema-validate')).toBe('schema-validate');
|
|
126
|
+
expect(describe_('staleness-gate')).toBe('staleness-gate');
|
|
127
|
+
expect(describe_('value-diff')).toBe('value-diff');
|
|
128
|
+
expect(describe_('none')).toBe('none');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('configManifestEntrySchema rejects invalid input', () => {
|
|
133
|
+
it('rejects empty id', () => {
|
|
134
|
+
const result = configManifestEntrySchema.safeParse({
|
|
135
|
+
id: '',
|
|
136
|
+
scope: 'project',
|
|
137
|
+
path: '.cleo/config.json',
|
|
138
|
+
mergePrecedence: 20,
|
|
139
|
+
driftDetection: 'schema-validate',
|
|
140
|
+
});
|
|
141
|
+
expect(result.success).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('rejects unknown scope', () => {
|
|
145
|
+
const result = configManifestEntrySchema.safeParse({
|
|
146
|
+
id: 'x',
|
|
147
|
+
scope: 'workspace',
|
|
148
|
+
path: '.cleo/x.json',
|
|
149
|
+
mergePrecedence: 0,
|
|
150
|
+
driftDetection: 'none',
|
|
151
|
+
});
|
|
152
|
+
expect(result.success).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects negative mergePrecedence', () => {
|
|
156
|
+
const result = configManifestEntrySchema.safeParse({
|
|
157
|
+
id: 'x',
|
|
158
|
+
scope: 'project',
|
|
159
|
+
path: '.cleo/x.json',
|
|
160
|
+
mergePrecedence: -1,
|
|
161
|
+
driftDetection: 'none',
|
|
162
|
+
});
|
|
163
|
+
expect(result.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects unknown driftDetection variant', () => {
|
|
167
|
+
const result = configManifestEntrySchema.safeParse({
|
|
168
|
+
id: 'x',
|
|
169
|
+
scope: 'project',
|
|
170
|
+
path: '.cleo/x.json',
|
|
171
|
+
mergePrecedence: 0,
|
|
172
|
+
driftDetection: 'sha-compare',
|
|
173
|
+
});
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|