@cleocode/contracts 2026.4.70 → 2026.4.73
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-record.test.d.ts +14 -0
- package/dist/__tests__/evidence-record.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-record.test.js +362 -0
- package/dist/__tests__/evidence-record.test.js.map +1 -0
- package/dist/__tests__/invariants.test.d.ts +19 -0
- package/dist/__tests__/invariants.test.d.ts.map +1 -0
- package/dist/__tests__/invariants.test.js +351 -0
- package/dist/__tests__/invariants.test.js.map +1 -0
- package/dist/acceptance-gate-schema.d.ts +81 -4
- package/dist/acceptance-gate-schema.d.ts.map +1 -1
- package/dist/acceptance-gate-schema.js +60 -1
- package/dist/acceptance-gate-schema.js.map +1 -1
- package/dist/acceptance-gate.d.ts +99 -2
- package/dist/acceptance-gate.d.ts.map +1 -1
- package/dist/evidence-record-schema.d.ts +202 -0
- package/dist/evidence-record-schema.d.ts.map +1 -0
- package/dist/evidence-record-schema.js +130 -0
- package/dist/evidence-record-schema.js.map +1 -0
- package/dist/evidence-record.d.ts +167 -0
- package/dist/evidence-record.d.ts.map +1 -0
- package/dist/evidence-record.js +21 -0
- package/dist/evidence-record.js.map +1 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/task-evidence.d.ts +251 -0
- package/dist/task-evidence.d.ts.map +1 -0
- package/dist/task-evidence.js +89 -0
- package/dist/task-evidence.js.map +1 -0
- package/package.json +8 -2
- package/schemas/acceptance-gate.schema.json +9 -0
- package/schemas/attachment.schema.json +9 -0
- package/schemas/gate-result-details.schema.json +9 -0
- package/schemas/gate-result.schema.json +9 -0
- package/schemas/manifest-v1.json +176 -0
- package/schemas/task-evidence.schema.json +9 -0
- package/schemas/task.schema.json +9 -0
- package/src/__tests__/evidence-record.test.ts +412 -0
- package/src/__tests__/invariants.test.ts +445 -0
- package/src/acceptance-gate-schema.ts +65 -1
- package/src/acceptance-gate.ts +108 -2
- package/src/evidence-record-schema.ts +158 -0
- package/src/evidence-record.ts +182 -0
- package/src/index.ts +65 -1
- package/src/task-evidence.ts +239 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://cleocode.dev/schemas/backup-manifest-v1.json",
|
|
4
|
+
"title": "CLEO Backup Manifest v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["$schema", "manifestVersion", "backup", "databases", "json", "integrity"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"const": "./schemas/manifest-v1.json"
|
|
12
|
+
},
|
|
13
|
+
"manifestVersion": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "^1\\.\\d+\\.\\d+$",
|
|
16
|
+
"description": "Semantic version of the manifest format. Breaking changes increment major."
|
|
17
|
+
},
|
|
18
|
+
"backup": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["createdAt", "createdBy", "scope", "machineFingerprint", "cleoVersion", "encrypted"],
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"properties": {
|
|
23
|
+
"createdAt": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"format": "date-time",
|
|
26
|
+
"description": "ISO 8601 UTC timestamp when the bundle was created."
|
|
27
|
+
},
|
|
28
|
+
"createdBy": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Human-readable cleo version string, e.g. 'cleo v2026.4.13'."
|
|
31
|
+
},
|
|
32
|
+
"scope": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["project", "global", "all"],
|
|
35
|
+
"description": "Export scope. Determines target restore paths."
|
|
36
|
+
},
|
|
37
|
+
"projectName": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Project name at export time. Advisory only."
|
|
40
|
+
},
|
|
41
|
+
"projectFingerprint": {
|
|
42
|
+
"type": ["string", "null"],
|
|
43
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
44
|
+
"description": "SHA-256 of project-info.json at export time. Null for global-only scope. Advisory warning if mismatch on import."
|
|
45
|
+
},
|
|
46
|
+
"machineFingerprint": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
49
|
+
"description": "SHA-256 of $XDG_DATA_HOME/cleo/machine-key. Privacy-safe machine identity proxy."
|
|
50
|
+
},
|
|
51
|
+
"cleoVersion": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Machine-parseable CalVer string, e.g. '2026.4.13'."
|
|
54
|
+
},
|
|
55
|
+
"encrypted": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"description": "True if this bundle was produced with --encrypt."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"databases": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"minItems": 0,
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": ["name", "filename", "size", "sha256", "schemaVersion"],
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"properties": {
|
|
69
|
+
"name": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": ["tasks", "brain", "conduit", "nexus", "signaldock"],
|
|
72
|
+
"description": "Logical database name."
|
|
73
|
+
},
|
|
74
|
+
"filename": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Relative path inside the archive, e.g. 'databases/tasks.db'."
|
|
77
|
+
},
|
|
78
|
+
"size": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"minimum": 0,
|
|
81
|
+
"description": "Byte size of the decompressed .db file."
|
|
82
|
+
},
|
|
83
|
+
"sha256": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
86
|
+
"description": "SHA-256 hex of the decompressed .db bytes."
|
|
87
|
+
},
|
|
88
|
+
"schemaVersion": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Latest applied migration identifier (Drizzle folderMillis or SIGNALDOCK_SCHEMA_VERSION)."
|
|
91
|
+
},
|
|
92
|
+
"rowCounts": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"description": "Per-table row counts for inspect display. Optional at import time.",
|
|
95
|
+
"additionalProperties": {
|
|
96
|
+
"type": "integer",
|
|
97
|
+
"minimum": 0
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"json": {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"minItems": 0,
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"required": ["filename", "size", "sha256"],
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"properties": {
|
|
111
|
+
"filename": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"enum": [
|
|
114
|
+
"json/config.json",
|
|
115
|
+
"json/project-info.json",
|
|
116
|
+
"json/project-context.json"
|
|
117
|
+
],
|
|
118
|
+
"description": "Relative path inside the archive."
|
|
119
|
+
},
|
|
120
|
+
"size": {
|
|
121
|
+
"type": "integer",
|
|
122
|
+
"minimum": 0
|
|
123
|
+
},
|
|
124
|
+
"sha256": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"globalFiles": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"minItems": 0,
|
|
134
|
+
"items": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"required": ["filename", "size", "sha256"],
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"properties": {
|
|
139
|
+
"filename": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"enum": ["global/global-salt"],
|
|
142
|
+
"description": "Relative path inside the archive."
|
|
143
|
+
},
|
|
144
|
+
"size": {
|
|
145
|
+
"type": "integer",
|
|
146
|
+
"minimum": 0
|
|
147
|
+
},
|
|
148
|
+
"sha256": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"integrity": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"required": ["algorithm", "checksumsFile", "manifestHash"],
|
|
158
|
+
"additionalProperties": false,
|
|
159
|
+
"properties": {
|
|
160
|
+
"algorithm": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"const": "sha256"
|
|
163
|
+
},
|
|
164
|
+
"checksumsFile": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"const": "checksums.sha256"
|
|
167
|
+
},
|
|
168
|
+
"manifestHash": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
171
|
+
"description": "SHA-256 of this manifest JSON with manifestHash set to empty string, then hex-encoded."
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "TaskEvidence",
|
|
4
|
+
"description": "Typed evidence artifact attached to a verification record — discriminated by kind",
|
|
5
|
+
"$ref": "#/definitions/TaskEvidence",
|
|
6
|
+
"definitions": {
|
|
7
|
+
"TaskEvidence": {}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip Zod parse tests for `EvidenceRecord` variants.
|
|
3
|
+
*
|
|
4
|
+
* Each variant is exercised with two cases:
|
|
5
|
+
* 1. A minimal (required-fields-only) input.
|
|
6
|
+
* 2. A maximal (all-fields populated) input with a structural type-check.
|
|
7
|
+
*
|
|
8
|
+
* Five variants × 2 cases = 10 core tests, plus union-routing and rejection tests.
|
|
9
|
+
*
|
|
10
|
+
* @epic T810
|
|
11
|
+
* @task T816
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import type {
|
|
16
|
+
CommandOutputRecord,
|
|
17
|
+
EvidenceRecord,
|
|
18
|
+
ImplDiffRecord,
|
|
19
|
+
LintReportRecord,
|
|
20
|
+
TestOutputRecord,
|
|
21
|
+
ValidateSpecCheckRecord,
|
|
22
|
+
} from '../evidence-record.js';
|
|
23
|
+
import {
|
|
24
|
+
commandOutputRecordSchema,
|
|
25
|
+
evidenceRecordSchema,
|
|
26
|
+
implDiffRecordSchema,
|
|
27
|
+
lintReportRecordSchema,
|
|
28
|
+
testOutputRecordSchema,
|
|
29
|
+
validateSpecCheckRecordSchema,
|
|
30
|
+
} from '../evidence-record-schema.js';
|
|
31
|
+
|
|
32
|
+
// ─── Shared fixture helpers ───────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const SHA256 = 'a'.repeat(64);
|
|
35
|
+
const AGENT = 'T816-worker';
|
|
36
|
+
const TIMESTAMP = '2026-04-16T00:00:00.000Z';
|
|
37
|
+
|
|
38
|
+
// ─── impl-diff ────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('implDiffRecordSchema', () => {
|
|
41
|
+
it('round-trips a minimal impl-diff record', () => {
|
|
42
|
+
const input = {
|
|
43
|
+
kind: 'impl-diff' as const,
|
|
44
|
+
phase: 'implement' as const,
|
|
45
|
+
agentIdentity: AGENT,
|
|
46
|
+
attachmentSha256: SHA256,
|
|
47
|
+
filesChanged: ['packages/contracts/src/evidence-record.ts'],
|
|
48
|
+
linesAdded: 120,
|
|
49
|
+
linesRemoved: 0,
|
|
50
|
+
ranAt: TIMESTAMP,
|
|
51
|
+
durationMs: 850,
|
|
52
|
+
};
|
|
53
|
+
const result = implDiffRecordSchema.parse(input);
|
|
54
|
+
expect(result.kind).toBe('impl-diff');
|
|
55
|
+
expect(result.phase).toBe('implement');
|
|
56
|
+
expect(result.filesChanged).toHaveLength(1);
|
|
57
|
+
expect(result.linesAdded).toBe(120);
|
|
58
|
+
expect(result.linesRemoved).toBe(0);
|
|
59
|
+
expect(result.durationMs).toBe(850);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('round-trips a maximal impl-diff record and type-checks as EvidenceRecord', () => {
|
|
63
|
+
const input = {
|
|
64
|
+
kind: 'impl-diff' as const,
|
|
65
|
+
phase: 'implement' as const,
|
|
66
|
+
agentIdentity: AGENT,
|
|
67
|
+
attachmentSha256: SHA256,
|
|
68
|
+
filesChanged: [
|
|
69
|
+
'packages/contracts/src/evidence-record.ts',
|
|
70
|
+
'packages/contracts/src/evidence-record-schema.ts',
|
|
71
|
+
'packages/contracts/src/index.ts',
|
|
72
|
+
],
|
|
73
|
+
linesAdded: 300,
|
|
74
|
+
linesRemoved: 5,
|
|
75
|
+
ranAt: TIMESTAMP,
|
|
76
|
+
durationMs: 1200,
|
|
77
|
+
};
|
|
78
|
+
const result = implDiffRecordSchema.parse(input);
|
|
79
|
+
expect(result.filesChanged).toHaveLength(3);
|
|
80
|
+
expect(result.linesRemoved).toBe(5);
|
|
81
|
+
// Structural compatibility with canonical interface
|
|
82
|
+
const typed: ImplDiffRecord = result;
|
|
83
|
+
expect(typed.kind).toBe('impl-diff');
|
|
84
|
+
// Assignable to the union
|
|
85
|
+
const unionTyped: EvidenceRecord = result;
|
|
86
|
+
expect(unionTyped.kind).toBe('impl-diff');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── validate-spec-check ─────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('validateSpecCheckRecordSchema', () => {
|
|
93
|
+
it('round-trips a minimal validate-spec-check record', () => {
|
|
94
|
+
const input = {
|
|
95
|
+
kind: 'validate-spec-check' as const,
|
|
96
|
+
phase: 'validate' as const,
|
|
97
|
+
agentIdentity: AGENT,
|
|
98
|
+
attachmentSha256: SHA256,
|
|
99
|
+
reqIdsChecked: ['IVTR-01'],
|
|
100
|
+
passed: true,
|
|
101
|
+
details: 'All checked REQ-IDs satisfied.',
|
|
102
|
+
ranAt: TIMESTAMP,
|
|
103
|
+
durationMs: 300,
|
|
104
|
+
};
|
|
105
|
+
const result = validateSpecCheckRecordSchema.parse(input);
|
|
106
|
+
expect(result.kind).toBe('validate-spec-check');
|
|
107
|
+
expect(result.phase).toBe('validate');
|
|
108
|
+
expect(result.passed).toBe(true);
|
|
109
|
+
expect(result.reqIdsChecked).toEqual(['IVTR-01']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('round-trips a maximal validate-spec-check record with failure', () => {
|
|
113
|
+
const input = {
|
|
114
|
+
kind: 'validate-spec-check' as const,
|
|
115
|
+
phase: 'validate' as const,
|
|
116
|
+
agentIdentity: AGENT,
|
|
117
|
+
attachmentSha256: SHA256,
|
|
118
|
+
reqIdsChecked: ['IVTR-01', 'IVTR-02', 'IVTR-06'],
|
|
119
|
+
passed: false,
|
|
120
|
+
details: 'IVTR-06: EvidenceRecord schema missing lint-report variant.',
|
|
121
|
+
ranAt: TIMESTAMP,
|
|
122
|
+
durationMs: 550,
|
|
123
|
+
};
|
|
124
|
+
const result = validateSpecCheckRecordSchema.parse(input);
|
|
125
|
+
expect(result.reqIdsChecked).toHaveLength(3);
|
|
126
|
+
expect(result.passed).toBe(false);
|
|
127
|
+
expect(result.details).toContain('lint-report');
|
|
128
|
+
// Structural compatibility with canonical interface
|
|
129
|
+
const typed: ValidateSpecCheckRecord = result;
|
|
130
|
+
expect(typed.kind).toBe('validate-spec-check');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── test-output ─────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
describe('testOutputRecordSchema', () => {
|
|
137
|
+
it('round-trips a minimal test-output record', () => {
|
|
138
|
+
const input = {
|
|
139
|
+
kind: 'test-output' as const,
|
|
140
|
+
phase: 'test' as const,
|
|
141
|
+
agentIdentity: AGENT,
|
|
142
|
+
attachmentSha256: SHA256,
|
|
143
|
+
command: 'pnpm --filter @cleocode/contracts run test',
|
|
144
|
+
exitCode: 0,
|
|
145
|
+
testsPassed: 10,
|
|
146
|
+
testsFailed: 0,
|
|
147
|
+
ranAt: TIMESTAMP,
|
|
148
|
+
durationMs: 4200,
|
|
149
|
+
};
|
|
150
|
+
const result = testOutputRecordSchema.parse(input);
|
|
151
|
+
expect(result.kind).toBe('test-output');
|
|
152
|
+
expect(result.phase).toBe('test');
|
|
153
|
+
expect(result.exitCode).toBe(0);
|
|
154
|
+
expect(result.testsPassed).toBe(10);
|
|
155
|
+
expect(result.testsFailed).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('round-trips a test-output record with failures and type-checks as EvidenceRecord', () => {
|
|
159
|
+
const input = {
|
|
160
|
+
kind: 'test-output' as const,
|
|
161
|
+
phase: 'test' as const,
|
|
162
|
+
agentIdentity: AGENT,
|
|
163
|
+
attachmentSha256: SHA256,
|
|
164
|
+
command: 'pnpm run test',
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
testsPassed: 8,
|
|
167
|
+
testsFailed: 2,
|
|
168
|
+
ranAt: TIMESTAMP,
|
|
169
|
+
durationMs: 3100,
|
|
170
|
+
};
|
|
171
|
+
const result = testOutputRecordSchema.parse(input);
|
|
172
|
+
expect(result.exitCode).toBe(1);
|
|
173
|
+
expect(result.testsFailed).toBe(2);
|
|
174
|
+
// Structural compatibility
|
|
175
|
+
const typed: TestOutputRecord = result;
|
|
176
|
+
expect(typed.command).toBe('pnpm run test');
|
|
177
|
+
const unionTyped: EvidenceRecord = result;
|
|
178
|
+
expect(unionTyped.kind).toBe('test-output');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── lint-report ─────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe('lintReportRecordSchema', () => {
|
|
185
|
+
it('round-trips a minimal lint-report record (implement phase)', () => {
|
|
186
|
+
const input = {
|
|
187
|
+
kind: 'lint-report' as const,
|
|
188
|
+
phase: 'implement' as const,
|
|
189
|
+
agentIdentity: AGENT,
|
|
190
|
+
attachmentSha256: SHA256,
|
|
191
|
+
tool: 'biome',
|
|
192
|
+
passed: true,
|
|
193
|
+
warnings: 0,
|
|
194
|
+
errors: 0,
|
|
195
|
+
ranAt: TIMESTAMP,
|
|
196
|
+
durationMs: 620,
|
|
197
|
+
};
|
|
198
|
+
const result = lintReportRecordSchema.parse(input);
|
|
199
|
+
expect(result.kind).toBe('lint-report');
|
|
200
|
+
expect(result.phase).toBe('implement');
|
|
201
|
+
expect(result.tool).toBe('biome');
|
|
202
|
+
expect(result.passed).toBe(true);
|
|
203
|
+
expect(result.warnings).toBe(0);
|
|
204
|
+
expect(result.errors).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('round-trips a lint-report record with warnings (test phase) and type-checks', () => {
|
|
208
|
+
const input = {
|
|
209
|
+
kind: 'lint-report' as const,
|
|
210
|
+
phase: 'test' as const,
|
|
211
|
+
agentIdentity: AGENT,
|
|
212
|
+
attachmentSha256: SHA256,
|
|
213
|
+
tool: 'tsc',
|
|
214
|
+
passed: false,
|
|
215
|
+
warnings: 3,
|
|
216
|
+
errors: 2,
|
|
217
|
+
ranAt: TIMESTAMP,
|
|
218
|
+
durationMs: 800,
|
|
219
|
+
};
|
|
220
|
+
const result = lintReportRecordSchema.parse(input);
|
|
221
|
+
expect(result.phase).toBe('test');
|
|
222
|
+
expect(result.warnings).toBe(3);
|
|
223
|
+
expect(result.errors).toBe(2);
|
|
224
|
+
expect(result.passed).toBe(false);
|
|
225
|
+
// Structural compatibility
|
|
226
|
+
const typed: LintReportRecord = result;
|
|
227
|
+
expect(typed.tool).toBe('tsc');
|
|
228
|
+
const unionTyped: EvidenceRecord = result;
|
|
229
|
+
expect(unionTyped.kind).toBe('lint-report');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ─── command-output ───────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
describe('commandOutputRecordSchema', () => {
|
|
236
|
+
it('round-trips a minimal command-output record (implement phase)', () => {
|
|
237
|
+
const input = {
|
|
238
|
+
kind: 'command-output' as const,
|
|
239
|
+
phase: 'implement' as const,
|
|
240
|
+
agentIdentity: AGENT,
|
|
241
|
+
attachmentSha256: SHA256,
|
|
242
|
+
cmd: 'git diff --stat HEAD',
|
|
243
|
+
exitCode: 0,
|
|
244
|
+
ranAt: TIMESTAMP,
|
|
245
|
+
durationMs: 120,
|
|
246
|
+
};
|
|
247
|
+
const result = commandOutputRecordSchema.parse(input);
|
|
248
|
+
expect(result.kind).toBe('command-output');
|
|
249
|
+
expect(result.phase).toBe('implement');
|
|
250
|
+
expect(result.cmd).toBe('git diff --stat HEAD');
|
|
251
|
+
expect(result.exitCode).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('round-trips a command-output record (validate phase) and type-checks as EvidenceRecord', () => {
|
|
255
|
+
const input = {
|
|
256
|
+
kind: 'command-output' as const,
|
|
257
|
+
phase: 'validate' as const,
|
|
258
|
+
agentIdentity: AGENT,
|
|
259
|
+
attachmentSha256: SHA256,
|
|
260
|
+
cmd: 'cleo check --task T816',
|
|
261
|
+
exitCode: 0,
|
|
262
|
+
ranAt: TIMESTAMP,
|
|
263
|
+
durationMs: 250,
|
|
264
|
+
};
|
|
265
|
+
const result = commandOutputRecordSchema.parse(input);
|
|
266
|
+
expect(result.phase).toBe('validate');
|
|
267
|
+
// Structural compatibility
|
|
268
|
+
const typed: CommandOutputRecord = result;
|
|
269
|
+
expect(typed.cmd).toBe('cleo check --task T816');
|
|
270
|
+
const unionTyped: EvidenceRecord = result;
|
|
271
|
+
expect(unionTyped.kind).toBe('command-output');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ─── evidenceRecordSchema (discriminated union routing) ──────────────────────
|
|
276
|
+
|
|
277
|
+
describe('evidenceRecordSchema (discriminated union)', () => {
|
|
278
|
+
it('routes impl-diff by kind', () => {
|
|
279
|
+
const result = evidenceRecordSchema.parse({
|
|
280
|
+
kind: 'impl-diff',
|
|
281
|
+
phase: 'implement',
|
|
282
|
+
agentIdentity: AGENT,
|
|
283
|
+
attachmentSha256: SHA256,
|
|
284
|
+
filesChanged: ['src/x.ts'],
|
|
285
|
+
linesAdded: 10,
|
|
286
|
+
linesRemoved: 2,
|
|
287
|
+
ranAt: TIMESTAMP,
|
|
288
|
+
durationMs: 100,
|
|
289
|
+
});
|
|
290
|
+
expect(result.kind).toBe('impl-diff');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('routes validate-spec-check by kind', () => {
|
|
294
|
+
const result = evidenceRecordSchema.parse({
|
|
295
|
+
kind: 'validate-spec-check',
|
|
296
|
+
phase: 'validate',
|
|
297
|
+
agentIdentity: AGENT,
|
|
298
|
+
attachmentSha256: SHA256,
|
|
299
|
+
reqIdsChecked: ['REQ-01'],
|
|
300
|
+
passed: true,
|
|
301
|
+
details: 'ok',
|
|
302
|
+
ranAt: TIMESTAMP,
|
|
303
|
+
durationMs: 100,
|
|
304
|
+
});
|
|
305
|
+
expect(result.kind).toBe('validate-spec-check');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('routes test-output by kind', () => {
|
|
309
|
+
const result = evidenceRecordSchema.parse({
|
|
310
|
+
kind: 'test-output',
|
|
311
|
+
phase: 'test',
|
|
312
|
+
agentIdentity: AGENT,
|
|
313
|
+
attachmentSha256: SHA256,
|
|
314
|
+
command: 'pnpm test',
|
|
315
|
+
exitCode: 0,
|
|
316
|
+
testsPassed: 5,
|
|
317
|
+
testsFailed: 0,
|
|
318
|
+
ranAt: TIMESTAMP,
|
|
319
|
+
durationMs: 100,
|
|
320
|
+
});
|
|
321
|
+
expect(result.kind).toBe('test-output');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('routes lint-report by kind', () => {
|
|
325
|
+
const result = evidenceRecordSchema.parse({
|
|
326
|
+
kind: 'lint-report',
|
|
327
|
+
phase: 'implement',
|
|
328
|
+
agentIdentity: AGENT,
|
|
329
|
+
attachmentSha256: SHA256,
|
|
330
|
+
tool: 'biome',
|
|
331
|
+
passed: true,
|
|
332
|
+
warnings: 0,
|
|
333
|
+
errors: 0,
|
|
334
|
+
ranAt: TIMESTAMP,
|
|
335
|
+
durationMs: 100,
|
|
336
|
+
});
|
|
337
|
+
expect(result.kind).toBe('lint-report');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('routes command-output by kind', () => {
|
|
341
|
+
const result = evidenceRecordSchema.parse({
|
|
342
|
+
kind: 'command-output',
|
|
343
|
+
phase: 'test',
|
|
344
|
+
agentIdentity: AGENT,
|
|
345
|
+
attachmentSha256: SHA256,
|
|
346
|
+
cmd: 'cleo doctor',
|
|
347
|
+
exitCode: 0,
|
|
348
|
+
ranAt: TIMESTAMP,
|
|
349
|
+
durationMs: 100,
|
|
350
|
+
});
|
|
351
|
+
expect(result.kind).toBe('command-output');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('rejects an unknown kind', () => {
|
|
355
|
+
expect(() =>
|
|
356
|
+
evidenceRecordSchema.parse({
|
|
357
|
+
kind: 'unknown-kind',
|
|
358
|
+
agentIdentity: AGENT,
|
|
359
|
+
attachmentSha256: SHA256,
|
|
360
|
+
ranAt: TIMESTAMP,
|
|
361
|
+
durationMs: 0,
|
|
362
|
+
}),
|
|
363
|
+
).toThrow();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('rejects an invalid attachmentSha256 (wrong length)', () => {
|
|
367
|
+
expect(() =>
|
|
368
|
+
evidenceRecordSchema.parse({
|
|
369
|
+
kind: 'command-output',
|
|
370
|
+
phase: 'test',
|
|
371
|
+
agentIdentity: AGENT,
|
|
372
|
+
attachmentSha256: 'tooshort',
|
|
373
|
+
cmd: 'cleo doctor',
|
|
374
|
+
exitCode: 0,
|
|
375
|
+
ranAt: TIMESTAMP,
|
|
376
|
+
durationMs: 0,
|
|
377
|
+
}),
|
|
378
|
+
).toThrow();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('rejects an invalid ranAt (non-datetime string)', () => {
|
|
382
|
+
expect(() =>
|
|
383
|
+
evidenceRecordSchema.parse({
|
|
384
|
+
kind: 'test-output',
|
|
385
|
+
phase: 'test',
|
|
386
|
+
agentIdentity: AGENT,
|
|
387
|
+
attachmentSha256: SHA256,
|
|
388
|
+
command: 'pnpm test',
|
|
389
|
+
exitCode: 0,
|
|
390
|
+
testsPassed: 1,
|
|
391
|
+
testsFailed: 0,
|
|
392
|
+
ranAt: 'not-a-timestamp',
|
|
393
|
+
durationMs: 0,
|
|
394
|
+
}),
|
|
395
|
+
).toThrow();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('rejects negative durationMs', () => {
|
|
399
|
+
expect(() =>
|
|
400
|
+
evidenceRecordSchema.parse({
|
|
401
|
+
kind: 'command-output',
|
|
402
|
+
phase: 'implement',
|
|
403
|
+
agentIdentity: AGENT,
|
|
404
|
+
attachmentSha256: SHA256,
|
|
405
|
+
cmd: 'cleo doctor',
|
|
406
|
+
exitCode: 0,
|
|
407
|
+
ranAt: TIMESTAMP,
|
|
408
|
+
durationMs: -1,
|
|
409
|
+
}),
|
|
410
|
+
).toThrow();
|
|
411
|
+
});
|
|
412
|
+
});
|