@cleocode/contracts 2026.5.110 → 2026.5.112
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__/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/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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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-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 +159 -0
- package/dist/invariants/index.d.ts.map +1 -0
- package/dist/invariants/index.js +109 -0
- package/dist/invariants/index.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/evidence-atom-schema.test.ts +499 -0
- package/src/evidence-atom-schema.ts +628 -0
- package/src/index.ts +39 -0
- package/src/invariants/__tests__/registry.test.ts +136 -0
- package/src/invariants/adr-073-saga.ts +167 -0
- package/src/invariants/index.ts +213 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/contracts",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.112",
|
|
4
4
|
"description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"dependencies": {
|
|
139
139
|
"zod": "^4.3.6",
|
|
140
140
|
"zod-to-json-schema": "^3.25.2",
|
|
141
|
-
"@cleocode/lafs": "2026.5.
|
|
141
|
+
"@cleocode/lafs": "2026.5.112"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc -b --force && node scripts/copy-assets.mjs && node scripts/emit-schemas.mjs",
|
|
@@ -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
|
+
});
|