ultra-builder-pro-cli 0.26.1 → 3.0.2
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/.ultra-template/contexts/TEMPLATE.md +64 -29
- package/.ultra-template/north-star.md +36 -30
- package/.ultra-template/project-brief.md +28 -0
- package/.ultra-template/specs/product.md +4 -0
- package/.ultra-template/specs/research-distillate.md +8 -0
- package/.ultra-template/tasks.json +1 -0
- package/.ultra-template/test-report.json +18 -1
- package/CHANGELOG.md +227 -1
- package/README.md +161 -59
- package/adapters/_shared/host-profile.cjs +68 -12
- package/adapters/_shared/plugin-core.cjs +4 -5
- package/adapters/_shared/runtime-assets.cjs +11 -2
- package/adapters/zcode.js +375 -0
- package/bin/delegate-worker.cjs +20 -0
- package/bin/delegate.cjs +94 -25
- package/bin/install.js +11 -6
- package/docs/ARCHITECTURE.md +109 -25
- package/docs/ARTIFACT-AUTHORITY.md +204 -30
- package/docs/DECISIONS.md +141 -10
- package/docs/PHILOSOPHY.md +61 -8
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
- package/docs/SKILL-AUTHORING.md +14 -9
- package/docs/WORKFLOW-LIFECYCLE.md +211 -53
- package/docs/evals/adversarial-review-2026-08-14.md +84 -0
- package/docs/evals/zcode-automation-2026-08-14.md +103 -0
- package/hooks/README.md +31 -3
- package/hooks/_common.py +1699 -33
- package/hooks/adapters/zcode.py +227 -0
- package/hooks/compact_context.py +176 -17
- package/hooks/mid_workflow_recall.py +27 -5
- package/hooks/post_edit_guard.py +133 -23
- package/hooks/session_context.py +722 -11
- package/package.json +6 -3
- package/skills/ultra-change/SKILL.md +84 -64
- package/skills/ultra-change/references/change-contract.md +130 -32
- package/skills/ultra-change/references/execution-grant.md +109 -0
- package/skills/ultra-change/references/primary-transfer.md +273 -0
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +1928 -0
- package/skills/ultra-delegate/SKILL.md +38 -5
- package/skills/ultra-delegate/references/delegation-contract.md +21 -1
- package/skills/ultra-deliver/SKILL.md +123 -44
- package/skills/ultra-deliver/references/baseline-reconciliation.md +5 -3
- package/skills/ultra-dev/SKILL.md +173 -43
- package/skills/ultra-grilling/SKILL.md +19 -16
- package/skills/ultra-grilling/references/reframing.md +7 -3
- package/skills/ultra-init/SKILL.md +99 -87
- package/skills/ultra-init/references/brownfield-adoption.md +23 -40
- package/skills/ultra-init/scripts/init_project.cjs +968 -21
- package/skills/ultra-plan/SKILL.md +83 -46
- package/skills/ultra-plan/references/task-evidence-v2.md +300 -0
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +859 -0
- package/skills/ultra-research/SKILL.md +119 -68
- package/skills/ultra-research/references/{03-competitive-landscape.md → 03-alternatives.md} +6 -6
- package/skills/ultra-research/references/04-product-strategy.md +11 -2
- package/skills/ultra-research/references/21-features-scope.md +8 -0
- package/skills/ultra-research/references/22-success-metrics.md +9 -4
- package/skills/ultra-research/references/99-synthesis.md +25 -1
- package/skills/ultra-research/references/north-star-v2.md +168 -0
- package/skills/ultra-research/references/wayfinding.md +50 -0
- package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
- package/skills/ultra-review/SKILL.md +201 -24
- package/skills/ultra-review/references/adversarial-evaluation.md +31 -0
- package/skills/ultra-review/references/spec.md +5 -0
- package/skills/ultra-review/references/unified-schema.md +146 -11
- package/skills/ultra-review/references/worker-packet.md +151 -8
- package/skills/ultra-review/scripts/review_wait.py +2653 -63
- package/skills/ultra-status/SKILL.md +146 -32
- package/skills/ultra-tdd/SKILL.md +14 -4
- package/skills/ultra-tdd/references/test-execution.md +6 -1
- package/skills/ultra-test/SKILL.md +157 -15
- package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +551 -29
- package/skills/ultra-think/SKILL.md +5 -10
- package/skills/ultra-think/references/interaction-boundary.md +2 -1
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
|
|
8
|
+
const MAX_EVIDENCE_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_IDENTIFIER_BYTES = 128;
|
|
11
|
+
const MAX_REPOSITORY_REF_BYTES = 4096;
|
|
12
|
+
|
|
13
|
+
const ROOT_KEYS = [
|
|
14
|
+
'$schema',
|
|
15
|
+
'task_id',
|
|
16
|
+
'change_id',
|
|
17
|
+
'context',
|
|
18
|
+
'subject',
|
|
19
|
+
'acceptance',
|
|
20
|
+
'dimensions',
|
|
21
|
+
'task_review',
|
|
22
|
+
'artifacts',
|
|
23
|
+
'limitations',
|
|
24
|
+
'timestamp',
|
|
25
|
+
];
|
|
26
|
+
const DIMENSION_KEYS = [
|
|
27
|
+
'feature_flags_audit',
|
|
28
|
+
'persistence_real',
|
|
29
|
+
'spec_trace',
|
|
30
|
+
'tests_passed',
|
|
31
|
+
'tests_written',
|
|
32
|
+
'vertical_slice',
|
|
33
|
+
];
|
|
34
|
+
const VERIFICATION_TYPES = new Set([
|
|
35
|
+
'command',
|
|
36
|
+
'inspection',
|
|
37
|
+
'owner-judgment',
|
|
38
|
+
'external-observation',
|
|
39
|
+
]);
|
|
40
|
+
const RESULT_TYPES = new Set(['satisfied', 'gap', 'not_applicable']);
|
|
41
|
+
const TASK_REVIEW_MODES = new Set(['strict-v4', 'external-manual']);
|
|
42
|
+
const EXTERNAL_RECEIPT_SCHEMA = 'ultra-external-review-receipt-v1';
|
|
43
|
+
const EXTERNAL_VERDICTS = new Set(['approve', 'request_changes']);
|
|
44
|
+
const FINDING_SEVERITIES = new Set(['P0', 'P1', 'P2', 'P3']);
|
|
45
|
+
const MAX_EXTERNAL_RECEIPT_BYTES = 8 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
function exactKeys(value, expected) {
|
|
48
|
+
return value !== null
|
|
49
|
+
&& typeof value === 'object'
|
|
50
|
+
&& !Array.isArray(value)
|
|
51
|
+
&& Object.keys(value).length === expected.length
|
|
52
|
+
&& expected.every((key) => Object.hasOwn(value, key));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nonempty(value) {
|
|
56
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeIdentifier(value) {
|
|
60
|
+
return typeof value === 'string'
|
|
61
|
+
&& Buffer.byteLength(value, 'utf8') <= MAX_IDENTIFIER_BYTES
|
|
62
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeRepositoryRef(value) {
|
|
66
|
+
if (!nonempty(value)
|
|
67
|
+
|| Buffer.byteLength(value, 'utf8') > MAX_REPOSITORY_REF_BYTES
|
|
68
|
+
|| value.includes('\\')
|
|
69
|
+
|| /[\u0000-\u001f\u007f]/u.test(value)
|
|
70
|
+
|| path.posix.isAbsolute(value)
|
|
71
|
+
|| path.win32.isAbsolute(value)
|
|
72
|
+
|| path.posix.normalize(value) !== value) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const segments = value.split('/');
|
|
76
|
+
return segments.length > 0
|
|
77
|
+
&& segments.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function digest(value, length) {
|
|
81
|
+
return typeof value === 'string'
|
|
82
|
+
&& new RegExp(`^[0-9a-f]{${length}}$`).test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function timestamp(value) {
|
|
86
|
+
if (!nonempty(value)) return false;
|
|
87
|
+
const match = value.match(
|
|
88
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/,
|
|
89
|
+
);
|
|
90
|
+
if (!match) return false;
|
|
91
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText,
|
|
92
|
+
offsetSign, offsetHourText, offsetMinuteText] = match;
|
|
93
|
+
const year = Number(yearText);
|
|
94
|
+
const month = Number(monthText);
|
|
95
|
+
const day = Number(dayText);
|
|
96
|
+
const hour = Number(hourText);
|
|
97
|
+
const minute = Number(minuteText);
|
|
98
|
+
const second = Number(secondText);
|
|
99
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
100
|
+
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
101
|
+
if (month < 1 || month > 12 || day < 1 || day > monthDays[month - 1]
|
|
102
|
+
|| hour > 23 || minute > 59 || second > 59) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (offsetSign
|
|
106
|
+
&& (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 59)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return Number.isFinite(Date.parse(value));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function strings(value, { nonemptyArray = false } = {}) {
|
|
113
|
+
return Array.isArray(value)
|
|
114
|
+
&& (!nonemptyArray || value.length > 0)
|
|
115
|
+
&& value.every(nonempty);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function diagnostic(diagnostics, code, at, message) {
|
|
119
|
+
diagnostics.push({ code, at, message });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inputFailure(code, inputPath, message) {
|
|
123
|
+
return {
|
|
124
|
+
valid: false,
|
|
125
|
+
classification: 'invalid',
|
|
126
|
+
diagnostics: [{ code, at: inputPath, message }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sameFileObservation(left, right) {
|
|
131
|
+
return left.dev === right.dev
|
|
132
|
+
&& left.ino === right.ino
|
|
133
|
+
&& left.mode === right.mode
|
|
134
|
+
&& left.size === right.size
|
|
135
|
+
&& left.mtimeNs === right.mtimeNs
|
|
136
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readBoundedStablePath(input) {
|
|
140
|
+
const resolved = path.resolve(input);
|
|
141
|
+
let beforePath;
|
|
142
|
+
try {
|
|
143
|
+
beforePath = fs.lstatSync(resolved, { bigint: true });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
failure: inputFailure(
|
|
147
|
+
'input_read_error',
|
|
148
|
+
resolved,
|
|
149
|
+
`Task evidence path could not be inspected: ${error.message}`,
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (beforePath.isSymbolicLink()) {
|
|
154
|
+
return {
|
|
155
|
+
failure: inputFailure(
|
|
156
|
+
'input_symlink',
|
|
157
|
+
resolved,
|
|
158
|
+
'Task evidence input must be a regular non-symlink file',
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!beforePath.isFile()) {
|
|
163
|
+
return {
|
|
164
|
+
failure: inputFailure(
|
|
165
|
+
'input_not_regular',
|
|
166
|
+
resolved,
|
|
167
|
+
'Task evidence input must be a regular non-symlink file',
|
|
168
|
+
),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (beforePath.size > BigInt(MAX_EVIDENCE_BYTES)) {
|
|
172
|
+
return {
|
|
173
|
+
failure: inputFailure(
|
|
174
|
+
'input_too_large',
|
|
175
|
+
resolved,
|
|
176
|
+
`Task evidence exceeds the ${MAX_EVIDENCE_BYTES}-byte snapshot limit`,
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let descriptor;
|
|
182
|
+
const chunks = [];
|
|
183
|
+
let observed = 0;
|
|
184
|
+
try {
|
|
185
|
+
descriptor = fs.openSync(
|
|
186
|
+
resolved,
|
|
187
|
+
fs.constants.O_RDONLY
|
|
188
|
+
| fs.constants.O_NONBLOCK
|
|
189
|
+
| (fs.constants.O_NOFOLLOW || 0),
|
|
190
|
+
);
|
|
191
|
+
const beforeDescriptor = fs.fstatSync(descriptor, { bigint: true });
|
|
192
|
+
if (!beforeDescriptor.isFile()
|
|
193
|
+
|| !sameFileObservation(beforePath, beforeDescriptor)) {
|
|
194
|
+
return {
|
|
195
|
+
failure: inputFailure(
|
|
196
|
+
'input_changed',
|
|
197
|
+
resolved,
|
|
198
|
+
'Task evidence changed before its stable byte snapshot could be read; retry after writes settle',
|
|
199
|
+
),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
while (true) {
|
|
204
|
+
const capacity = Math.min(
|
|
205
|
+
READ_CHUNK_BYTES,
|
|
206
|
+
MAX_EVIDENCE_BYTES + 1 - observed,
|
|
207
|
+
);
|
|
208
|
+
const chunk = Buffer.allocUnsafe(capacity);
|
|
209
|
+
const count = fs.readSync(descriptor, chunk, 0, capacity, null);
|
|
210
|
+
if (count === 0) break;
|
|
211
|
+
observed += count;
|
|
212
|
+
if (observed > MAX_EVIDENCE_BYTES) {
|
|
213
|
+
return {
|
|
214
|
+
failure: inputFailure(
|
|
215
|
+
'input_too_large',
|
|
216
|
+
resolved,
|
|
217
|
+
`Task evidence exceeds the ${MAX_EVIDENCE_BYTES}-byte snapshot limit`,
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
chunks.push(Buffer.from(chunk.subarray(0, count)));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const afterDescriptor = fs.fstatSync(descriptor, { bigint: true });
|
|
225
|
+
const afterPath = fs.lstatSync(resolved, { bigint: true });
|
|
226
|
+
if (afterPath.isSymbolicLink()
|
|
227
|
+
|| !afterPath.isFile()
|
|
228
|
+
|| !sameFileObservation(beforeDescriptor, afterDescriptor)
|
|
229
|
+
|| !sameFileObservation(afterDescriptor, afterPath)
|
|
230
|
+
|| BigInt(observed) !== afterDescriptor.size) {
|
|
231
|
+
return {
|
|
232
|
+
failure: inputFailure(
|
|
233
|
+
'input_changed',
|
|
234
|
+
resolved,
|
|
235
|
+
'Task evidence changed while its stable byte snapshot was read; retry after writes settle',
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { resolved, bytes: Buffer.concat(chunks, observed) };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
failure: inputFailure(
|
|
243
|
+
'input_read_error',
|
|
244
|
+
resolved,
|
|
245
|
+
`Task evidence stable byte snapshot could not be read: ${error.message}`,
|
|
246
|
+
),
|
|
247
|
+
};
|
|
248
|
+
} finally {
|
|
249
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseCapturedJson(snapshot) {
|
|
254
|
+
let text;
|
|
255
|
+
try {
|
|
256
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(snapshot.bytes);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
failure: inputFailure(
|
|
260
|
+
'input_invalid_utf8',
|
|
261
|
+
snapshot.resolved,
|
|
262
|
+
`Task evidence must be strict UTF-8: ${error.message}`,
|
|
263
|
+
),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
return { value: JSON.parse(text) };
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return {
|
|
270
|
+
failure: inputFailure(
|
|
271
|
+
'input_invalid_json',
|
|
272
|
+
snapshot.resolved,
|
|
273
|
+
`Task evidence must be valid JSON: ${error.message}`,
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function directoryIdentity(stat) {
|
|
280
|
+
return `${stat.dev}:${stat.ino}:${stat.mode}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function labelFailure(label, base, at, message) {
|
|
284
|
+
return {
|
|
285
|
+
valid: false,
|
|
286
|
+
classification: 'invalid',
|
|
287
|
+
diagnostics: [{ code: `${label}_${base}`, at, message }],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readRepositoryStableFile(repoRoot, ref, label) {
|
|
292
|
+
// Bounded repository-chain snapshot: the repository root is resolved to its
|
|
293
|
+
// real path (host-level environment links above the root are not managed
|
|
294
|
+
// surface), then the root itself and every managed parent below it must stay
|
|
295
|
+
// ordinary non-symlink directories, the final entry an ordinary regular file
|
|
296
|
+
// opened no-follow, and identities are rechecked after reading. A symlinked
|
|
297
|
+
// or replaced managed ancestor is typed rejection, never followed.
|
|
298
|
+
let root;
|
|
299
|
+
try {
|
|
300
|
+
root = fs.realpathSync(path.resolve(repoRoot));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return {
|
|
303
|
+
failure: labelFailure(label, 'read_error', ref, `${label} repository root is unavailable: ${error.message}`),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const segments = ref.split('/');
|
|
307
|
+
const identities = [];
|
|
308
|
+
try {
|
|
309
|
+
{
|
|
310
|
+
const before = fs.lstatSync(root);
|
|
311
|
+
if (before.isSymbolicLink() || !before.isDirectory()) {
|
|
312
|
+
return {
|
|
313
|
+
failure: labelFailure(label, 'ancestor_not_ordinary', ref, `${label} repository root must be an ordinary non-symlink directory`),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
identities.push([root, directoryIdentity(before)]);
|
|
317
|
+
}
|
|
318
|
+
let node = root;
|
|
319
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
320
|
+
node = path.join(node, segments[index]);
|
|
321
|
+
const before = fs.lstatSync(node);
|
|
322
|
+
if (before.isSymbolicLink() || !before.isDirectory()) {
|
|
323
|
+
return {
|
|
324
|
+
failure: labelFailure(label, 'ancestor_not_ordinary', ref, `${label} path parents must be ordinary non-symlink directories`),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
identities.push([node, directoryIdentity(before)]);
|
|
328
|
+
}
|
|
329
|
+
const filePath = path.join(node, segments[segments.length - 1]);
|
|
330
|
+
const snapshot = readBoundedStablePath(filePath);
|
|
331
|
+
if (snapshot.failure) {
|
|
332
|
+
const base = snapshot.failure.diagnostics[0];
|
|
333
|
+
const mapped = base.code === 'input_read_error' && /ENOENT/.test(base.message)
|
|
334
|
+
? 'missing'
|
|
335
|
+
: base.code.replace(/^input_/, '');
|
|
336
|
+
return { failure: labelFailure(label, mapped, ref, `${label} could not be observed (${mapped}): ${base.message}`) };
|
|
337
|
+
}
|
|
338
|
+
for (const [directory, identity] of identities) {
|
|
339
|
+
const after = fs.lstatSync(directory);
|
|
340
|
+
if (after.isSymbolicLink() || !after.isDirectory()
|
|
341
|
+
|| directoryIdentity(after) !== identity) {
|
|
342
|
+
return {
|
|
343
|
+
failure: labelFailure(label, 'ancestor_replaced', ref, `${label} path changed during its stable snapshot; retry after writes settle`),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { resolved: filePath, bytes: snapshot.bytes };
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error && error.code === 'ENOENT') {
|
|
350
|
+
return { failure: labelFailure(label, 'missing', ref, `${label} is missing below the repository root`) };
|
|
351
|
+
}
|
|
352
|
+
return { failure: labelFailure(label, 'read_error', ref, `${label} could not be observed: ${error.message}`) };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function validateFreshness(value, diagnostics, at) {
|
|
357
|
+
if (!exactKeys(value, ['git_head', 'worktree_digest', 'observed_at'])) {
|
|
358
|
+
diagnostic(diagnostics, 'freshness_shape', at, 'Expected exact git_head, worktree_digest, and observed_at fields');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!digest(value.git_head, 40)) {
|
|
362
|
+
diagnostic(diagnostics, 'freshness_shape', `${at}.git_head`, 'Expected a lowercase 40-character Git digest');
|
|
363
|
+
}
|
|
364
|
+
if (!digest(value.worktree_digest, 64)) {
|
|
365
|
+
diagnostic(diagnostics, 'freshness_shape', `${at}.worktree_digest`, 'Expected a lowercase 64-character worktree digest');
|
|
366
|
+
}
|
|
367
|
+
if (!timestamp(value.observed_at)) {
|
|
368
|
+
diagnostic(diagnostics, 'freshness_shape', `${at}.observed_at`, 'Expected an RFC 3339 observation time');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function validateEvidence(value, verificationType, diagnostics, at) {
|
|
373
|
+
if (verificationType === 'command') {
|
|
374
|
+
if (!exactKeys(value, [
|
|
375
|
+
'command', 'cwd', 'exit_code', 'raw_evidence_ref', 'raw_evidence_sha256',
|
|
376
|
+
'freshness_identity',
|
|
377
|
+
])) {
|
|
378
|
+
diagnostic(diagnostics, 'evidence_shape', at, 'Command evidence has the wrong fields');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!nonempty(value.command) || !nonempty(value.cwd)
|
|
382
|
+
|| !Number.isInteger(value.exit_code) || !digest(value.raw_evidence_sha256, 64)) {
|
|
383
|
+
diagnostic(diagnostics, 'evidence_shape', at, 'Command evidence requires command, cwd, integer exit_code, and a lowercase raw_evidence_sha256');
|
|
384
|
+
}
|
|
385
|
+
if (!safeRepositoryRef(value.raw_evidence_ref)) {
|
|
386
|
+
diagnostic(diagnostics, 'evidence_ref_shape', `${at}.raw_evidence_ref`, 'raw_evidence_ref must be one normalized repository-relative file path');
|
|
387
|
+
}
|
|
388
|
+
validateFreshness(value.freshness_identity, diagnostics, `${at}.freshness_identity`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (verificationType === 'inspection') {
|
|
393
|
+
if (!exactKeys(value, ['source', 'observation', 'revision'])
|
|
394
|
+
|| !nonempty(value.source) || !nonempty(value.observation) || !nonempty(value.revision)) {
|
|
395
|
+
diagnostic(diagnostics, 'evidence_shape', at, 'Inspection evidence requires exact non-empty source, observation, and revision fields');
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (verificationType === 'owner-judgment') {
|
|
401
|
+
if (!exactKeys(value, ['owner_record_ref', 'owner_statement_or_disposition'])
|
|
402
|
+
|| !nonempty(value.owner_record_ref) || !nonempty(value.owner_statement_or_disposition)) {
|
|
403
|
+
diagnostic(diagnostics, 'evidence_shape', at, 'Owner judgment requires a durable owner record and non-empty statement or disposition');
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!exactKeys(value, [
|
|
409
|
+
'provider', 'run_id', 'observed_at', 'raw_evidence_ref',
|
|
410
|
+
'raw_evidence_sha256', 'observation',
|
|
411
|
+
]) || !nonempty(value.provider) || !nonempty(value.run_id)
|
|
412
|
+
|| !timestamp(value.observed_at) || !digest(value.raw_evidence_sha256, 64)
|
|
413
|
+
|| !nonempty(value.observation)) {
|
|
414
|
+
diagnostic(diagnostics, 'evidence_shape', at, 'External observation requires exact provider, run_id, observed_at, raw_evidence_ref, raw_evidence_sha256, and observation fields');
|
|
415
|
+
}
|
|
416
|
+
if (exactKeys(value, [
|
|
417
|
+
'provider', 'run_id', 'observed_at', 'raw_evidence_ref',
|
|
418
|
+
'raw_evidence_sha256', 'observation',
|
|
419
|
+
]) && !safeRepositoryRef(value.raw_evidence_ref)) {
|
|
420
|
+
diagnostic(diagnostics, 'evidence_ref_shape', `${at}.raw_evidence_ref`, 'raw_evidence_ref must be one normalized repository-relative file path');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function validateAcceptance(value, diagnostics) {
|
|
425
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
426
|
+
diagnostic(diagnostics, 'acceptance_shape', 'acceptance', 'Acceptance must be a non-empty array');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const ids = new Set();
|
|
430
|
+
value.forEach((item, index) => {
|
|
431
|
+
const at = `acceptance[${index}]`;
|
|
432
|
+
if (!exactKeys(item, [
|
|
433
|
+
'criterion_id', 'verification_type', 'evidence', 'disposition',
|
|
434
|
+
])) {
|
|
435
|
+
diagnostic(diagnostics, 'acceptance_shape', at, 'Acceptance item has the wrong fields');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!nonempty(item.criterion_id) || ids.has(item.criterion_id)) {
|
|
439
|
+
diagnostic(diagnostics, 'acceptance_shape', `${at}.criterion_id`, 'Criterion ids must be non-empty and unique');
|
|
440
|
+
} else {
|
|
441
|
+
ids.add(item.criterion_id);
|
|
442
|
+
}
|
|
443
|
+
if (!VERIFICATION_TYPES.has(item.verification_type)) {
|
|
444
|
+
diagnostic(diagnostics, 'verification_type', `${at}.verification_type`, 'Unknown verification type');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
validateEvidence(item.evidence, item.verification_type, diagnostics, `${at}.evidence`);
|
|
448
|
+
if (!exactKeys(item.disposition, ['authority', 'result', 'rationale'])
|
|
449
|
+
|| !['model', 'owner'].includes(item.disposition?.authority)
|
|
450
|
+
|| !RESULT_TYPES.has(item.disposition?.result)
|
|
451
|
+
|| !nonempty(item.disposition?.rationale)) {
|
|
452
|
+
diagnostic(diagnostics, 'disposition_shape', `${at}.disposition`, 'Disposition requires authority, result, and rationale');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (item.verification_type === 'owner-judgment'
|
|
456
|
+
&& item.disposition.authority !== 'owner') {
|
|
457
|
+
diagnostic(diagnostics, 'owner_authority_required', `${at}.disposition.authority`, 'Only the owner may disposition owner-judgment acceptance');
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function validateDimensions(value, diagnostics) {
|
|
463
|
+
if (!exactKeys(value, DIMENSION_KEYS)) {
|
|
464
|
+
diagnostic(diagnostics, 'dimensions_shape', 'dimensions', 'Expected exactly the six coverage dimensions');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const name of DIMENSION_KEYS) {
|
|
468
|
+
const dimension = value[name];
|
|
469
|
+
if (!exactKeys(dimension, ['status', 'evidence_refs', 'rationale'])
|
|
470
|
+
|| !RESULT_TYPES.has(dimension?.status)
|
|
471
|
+
|| !strings(dimension?.evidence_refs)
|
|
472
|
+
|| !nonempty(dimension?.rationale)) {
|
|
473
|
+
diagnostic(diagnostics, 'dimensions_shape', `dimensions.${name}`, 'Dimension requires status, evidence_refs, and rationale');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function validateTaskReview(value, diagnostics, taskId) {
|
|
479
|
+
const mode = Object.hasOwn(value, 'review_mode') ? value.review_mode : 'strict-v4';
|
|
480
|
+
if (!TASK_REVIEW_MODES.has(mode)) {
|
|
481
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.review_mode', 'Unknown task review mode');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const sharedKeys = ['execution_packet', 'blocking_findings', 'retention'];
|
|
485
|
+
const strictKeys = mode === 'strict-v4'
|
|
486
|
+
? (Object.hasOwn(value, 'review_mode')
|
|
487
|
+
? ['review_mode', ...sharedKeys.slice(0, 1), 'session_id', 'summary_ref', 'summary_digest', ...sharedKeys.slice(1)]
|
|
488
|
+
: [...sharedKeys.slice(0, 1), 'session_id', 'summary_ref', 'summary_digest', ...sharedKeys.slice(1)])
|
|
489
|
+
: ['review_mode', ...sharedKeys.slice(0, 1), 'receipt_ref', 'receipt_sha256', ...sharedKeys.slice(1)];
|
|
490
|
+
if (!exactKeys(value, strictKeys)) {
|
|
491
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review', `Task review has the wrong fields for the ${mode} branch`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const packet = value.execution_packet;
|
|
495
|
+
if (!exactKeys(packet, ['state', 'digest', 'limitation'])) {
|
|
496
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.execution_packet', 'Execution packet has the wrong fields');
|
|
497
|
+
} else if (packet.state === 'pre-v1-unavailable') {
|
|
498
|
+
if (packet.digest !== null || !nonempty(packet.limitation)) {
|
|
499
|
+
diagnostic(diagnostics, 'bootstrap_packet_limitation', 'task_review.execution_packet', 'pre-v1-unavailable requires digest null and a non-empty limitation');
|
|
500
|
+
}
|
|
501
|
+
} else if (packet.state === 'available') {
|
|
502
|
+
if (!digest(packet.digest, 64) || (packet.limitation !== null && !nonempty(packet.limitation))) {
|
|
503
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.execution_packet', 'available requires a packet digest and null or non-empty limitation');
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.execution_packet.state', 'Unknown execution packet state');
|
|
507
|
+
}
|
|
508
|
+
if (!nonempty(value.retention)) {
|
|
509
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.retention', 'Task review requires a retention instruction');
|
|
510
|
+
}
|
|
511
|
+
if (mode === 'strict-v4') {
|
|
512
|
+
const validSessionId = safeIdentifier(value.session_id);
|
|
513
|
+
if (!validSessionId) {
|
|
514
|
+
diagnostic(diagnostics, 'identity_shape', 'task_review.session_id', 'Review session_id must be one safe identifier component');
|
|
515
|
+
}
|
|
516
|
+
if (!nonempty(value.summary_ref)
|
|
517
|
+
|| !digest(value.summary_digest, 64)) {
|
|
518
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review', 'Task review requires a session, summary identity, and retention instruction');
|
|
519
|
+
}
|
|
520
|
+
if (validSessionId
|
|
521
|
+
&& value.summary_ref !== `.ultra/reviews/${value.session_id}/SUMMARY.json`) {
|
|
522
|
+
diagnostic(diagnostics, 'review_summary_identity', 'task_review.summary_ref', 'Review summary_ref must exactly match the declared session_id');
|
|
523
|
+
}
|
|
524
|
+
} else if (!safeRepositoryRef(value.receipt_ref)
|
|
525
|
+
|| value.receipt_ref.startsWith('.ultra/reviews/')
|
|
526
|
+
|| !value.receipt_ref.endsWith('.json')) {
|
|
527
|
+
diagnostic(diagnostics, 'receipt_ref_shape', 'task_review.receipt_ref', 'External receipt ref must be one normalized repository-relative JSON file path outside .ultra/reviews');
|
|
528
|
+
} else if (safeIdentifier(taskId)
|
|
529
|
+
&& !new RegExp(`^\\.ultra/evidence/${taskId}/[A-Za-z0-9][A-Za-z0-9._-]*\\.json$`).test(value.receipt_ref)) {
|
|
530
|
+
diagnostic(diagnostics, 'receipt_ref_shape', 'task_review.receipt_ref', 'External receipt ref must live inside the task evidence directory of the declared task_id');
|
|
531
|
+
}
|
|
532
|
+
if (mode === 'external-manual' && !digest(value.receipt_sha256, 64)) {
|
|
533
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.receipt_sha256', 'External receipt binding requires a lowercase SHA-256 digest of the exact receipt bytes');
|
|
534
|
+
}
|
|
535
|
+
if (!Array.isArray(value.blocking_findings)) {
|
|
536
|
+
diagnostic(diagnostics, 'task_review_shape', 'task_review.blocking_findings', 'Blocking findings must be an array');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const ids = new Set();
|
|
540
|
+
value.blocking_findings.forEach((finding, index) => {
|
|
541
|
+
const at = `task_review.blocking_findings[${index}]`;
|
|
542
|
+
if (!exactKeys(finding, [
|
|
543
|
+
'id', 'resolution', 'disposition', 'evidence_refresh_refs',
|
|
544
|
+
]) || !nonempty(finding?.id) || ids.has(finding?.id)
|
|
545
|
+
|| !nonempty(finding?.resolution) || !nonempty(finding?.disposition)
|
|
546
|
+
|| !strings(finding?.evidence_refresh_refs, { nonemptyArray: true })) {
|
|
547
|
+
diagnostic(diagnostics, 'task_review_shape', at, 'Blocking finding requires a unique id, resolution, disposition, and evidence refresh refs');
|
|
548
|
+
}
|
|
549
|
+
if (nonempty(finding?.id)) ids.add(finding.id);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function findRepositoryRoot(evidencePath) {
|
|
554
|
+
let current = path.resolve(path.dirname(evidencePath));
|
|
555
|
+
while (true) {
|
|
556
|
+
if (fs.existsSync(path.join(current, '.ultra'))) return current;
|
|
557
|
+
const parent = path.dirname(current);
|
|
558
|
+
if (parent === current) return null;
|
|
559
|
+
current = parent;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function validateExternalReceipt(receipt, evidence, diagnostics) {
|
|
564
|
+
if (receipt === null || typeof receipt !== 'object' || Array.isArray(receipt)) {
|
|
565
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt', 'External receipt must be a JSON object');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (receipt.$schema !== EXTERNAL_RECEIPT_SCHEMA) {
|
|
569
|
+
diagnostic(diagnostics, 'receipt_schema', 'external_receipt.$schema', `External receipt must use ${EXTERNAL_RECEIPT_SCHEMA}; a strict review SUMMARY is not an external receipt`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!exactKeys(receipt, [
|
|
573
|
+
'$schema', 'reviewer', 'reviewer_role', 'task_id', 'change_id',
|
|
574
|
+
'reviewer_authority', 'reviewed_contract', 'subject', 'verdict', 'findings',
|
|
575
|
+
'timestamp',
|
|
576
|
+
])) {
|
|
577
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt', 'External receipt has the wrong fields');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!nonempty(receipt.reviewer) || receipt.reviewer_role !== 'read-only') {
|
|
581
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.reviewer', 'External receipt must name its reviewer and record the read-only role');
|
|
582
|
+
}
|
|
583
|
+
if (!safeIdentifier(receipt.task_id) || !safeIdentifier(receipt.change_id)) {
|
|
584
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.identity', 'External receipt task_id and change_id must be safe identifiers');
|
|
585
|
+
}
|
|
586
|
+
for (const [label, binding] of [
|
|
587
|
+
['reviewer_authority', receipt.reviewer_authority],
|
|
588
|
+
['reviewed_contract', receipt.reviewed_contract],
|
|
589
|
+
]) {
|
|
590
|
+
if (!exactKeys(binding, ['ref', 'sha256'])
|
|
591
|
+
|| !safeRepositoryRef(binding?.ref)
|
|
592
|
+
|| !digest(binding?.sha256, 64)) {
|
|
593
|
+
diagnostic(diagnostics, 'receipt_shape', `external_receipt.${label}`, `${label} needs a safe repository ref and lowercase SHA-256`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!digest(receipt.subject?.git_head, 40) || !digest(receipt.subject?.worktree_digest, 64)) {
|
|
597
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.subject', 'Reviewed subject needs a lowercase 40-hex Git HEAD and 64-hex product-worktree digest');
|
|
598
|
+
}
|
|
599
|
+
if (!EXTERNAL_VERDICTS.has(receipt.verdict)) {
|
|
600
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.verdict', 'External receipt verdict must be approve or request_changes');
|
|
601
|
+
}
|
|
602
|
+
if (!timestamp(receipt.timestamp)) {
|
|
603
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.timestamp', 'External receipt needs an RFC 3339 timestamp');
|
|
604
|
+
}
|
|
605
|
+
if (receipt.task_id !== evidence.task_id || receipt.change_id !== evidence.change_id) {
|
|
606
|
+
diagnostic(diagnostics, 'receipt_identity_mismatch', 'external_receipt', 'External receipt task_id/change_id must match the evidence record exactly');
|
|
607
|
+
}
|
|
608
|
+
if (digest(receipt.subject?.git_head, 40)
|
|
609
|
+
&& receipt.subject.git_head !== evidence.subject?.git_head) {
|
|
610
|
+
diagnostic(diagnostics, 'receipt_subject_mismatch', 'external_receipt.subject.git_head', 'External receipt reviewed HEAD must match the evidence completion subject HEAD');
|
|
611
|
+
}
|
|
612
|
+
if (digest(receipt.subject?.worktree_digest, 64)
|
|
613
|
+
&& receipt.subject.worktree_digest !== evidence.subject?.worktree_digest) {
|
|
614
|
+
diagnostic(diagnostics, 'receipt_subject_mismatch', 'external_receipt.subject.worktree_digest', 'External receipt reviewed product-worktree digest must match the evidence completion subject digest');
|
|
615
|
+
}
|
|
616
|
+
const severityById = new Map();
|
|
617
|
+
if (Array.isArray(receipt.findings)) {
|
|
618
|
+
receipt.findings.forEach((finding, index) => {
|
|
619
|
+
if (!exactKeys(finding, ['id', 'severity', 'title'])
|
|
620
|
+
|| !nonempty(finding?.id) || !nonempty(finding?.title)
|
|
621
|
+
|| !FINDING_SEVERITIES.has(finding?.severity)) {
|
|
622
|
+
diagnostic(diagnostics, 'receipt_shape', `external_receipt.findings[${index}]`, 'External finding requires id, P0-P3 severity, and title');
|
|
623
|
+
}
|
|
624
|
+
if (nonempty(finding?.id)) {
|
|
625
|
+
if (severityById.has(finding.id)) {
|
|
626
|
+
diagnostic(diagnostics, 'receipt_shape', `external_receipt.findings[${index}].id`, 'External finding ids must be unique');
|
|
627
|
+
}
|
|
628
|
+
severityById.set(finding.id, finding?.severity);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
} else {
|
|
632
|
+
diagnostic(diagnostics, 'receipt_shape', 'external_receipt.findings', 'External findings must be an array');
|
|
633
|
+
}
|
|
634
|
+
const blockingIds = new Set(
|
|
635
|
+
(evidence.task_review?.blocking_findings ?? [])
|
|
636
|
+
.map((finding) => finding?.id)
|
|
637
|
+
.filter((id) => typeof id === 'string' && id.length > 0),
|
|
638
|
+
);
|
|
639
|
+
const receiptBlockingIds = new Set(
|
|
640
|
+
[...severityById.entries()]
|
|
641
|
+
.filter(([, severity]) => severity === 'P0' || severity === 'P1')
|
|
642
|
+
.map(([id]) => id),
|
|
643
|
+
);
|
|
644
|
+
for (const id of receiptBlockingIds) {
|
|
645
|
+
if (!blockingIds.has(id)) {
|
|
646
|
+
diagnostic(diagnostics, 'receipt_blocking_set_unbound', `external_receipt.findings.${id}`, 'Every current P0/P1 receipt finding must have a matching blocking disposition');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (const id of blockingIds) {
|
|
650
|
+
const severity = severityById.get(id);
|
|
651
|
+
if (severity === undefined) {
|
|
652
|
+
diagnostic(diagnostics, 'receipt_blocking_set_unbound', `task_review.blocking_findings.${id}`, 'Every blocking disposition must name a finding carried by the external receipt');
|
|
653
|
+
} else if (severity !== 'P0' && severity !== 'P1') {
|
|
654
|
+
diagnostic(diagnostics, 'receipt_blocking_set_unbound', `task_review.blocking_findings.${id}`, 'Blocking dispositions may bind only receipt findings whose severity is P0 or P1');
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (receipt.verdict === 'approve' && receiptBlockingIds.size > 0) {
|
|
658
|
+
diagnostic(diagnostics, 'receipt_verdict_inconsistent', 'external_receipt.verdict', 'An approve verdict cannot carry unresolved P0/P1 findings');
|
|
659
|
+
}
|
|
660
|
+
if (receipt.verdict === 'request_changes' && receiptBlockingIds.size === 0) {
|
|
661
|
+
diagnostic(diagnostics, 'receipt_verdict_inconsistent', 'external_receipt.verdict', 'A request_changes verdict must carry at least one current P0/P1 finding');
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function verifyExternalReceipt(evidencePath, evidence) {
|
|
666
|
+
const diagnostics = [];
|
|
667
|
+
if ((evidence?.task_review?.review_mode ?? 'strict-v4') !== 'external-manual') {
|
|
668
|
+
diagnostic(diagnostics, 'unsupported_review_branch', 'task_review.review_mode', 'External receipt verification applies only to the external-manual branch; strict-v4 summaries use the review waiter');
|
|
669
|
+
return diagnostics;
|
|
670
|
+
}
|
|
671
|
+
const repositoryRoot = findRepositoryRoot(evidencePath);
|
|
672
|
+
if (repositoryRoot === null) {
|
|
673
|
+
diagnostic(diagnostics, 'receipt_read_error', '$', 'External receipt could not be resolved: no repository root with a .ultra directory was found above the evidence file');
|
|
674
|
+
return diagnostics;
|
|
675
|
+
}
|
|
676
|
+
const receiptSnapshot = readRepositoryStableFile(
|
|
677
|
+
repositoryRoot,
|
|
678
|
+
evidence.task_review.receipt_ref,
|
|
679
|
+
'receipt',
|
|
680
|
+
);
|
|
681
|
+
if (receiptSnapshot.failure) {
|
|
682
|
+
diagnostics.push(...receiptSnapshot.failure.diagnostics);
|
|
683
|
+
return diagnostics;
|
|
684
|
+
}
|
|
685
|
+
const observedDigest = crypto.createHash('sha256').update(receiptSnapshot.bytes).digest('hex');
|
|
686
|
+
if (observedDigest !== evidence.task_review.receipt_sha256) {
|
|
687
|
+
diagnostic(diagnostics, 'receipt_digest_mismatch', evidence.task_review.receipt_ref, 'Recorded receipt_sha256 does not match the observed stable receipt bytes');
|
|
688
|
+
}
|
|
689
|
+
let text;
|
|
690
|
+
try {
|
|
691
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(receiptSnapshot.bytes);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
diagnostic(diagnostics, 'receipt_invalid_utf8', evidence.task_review.receipt_ref, `External review receipt must be strict UTF-8: ${error.message}`);
|
|
694
|
+
return diagnostics;
|
|
695
|
+
}
|
|
696
|
+
let receipt;
|
|
697
|
+
try {
|
|
698
|
+
receipt = JSON.parse(text);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
diagnostic(diagnostics, 'receipt_invalid_json', evidence.task_review.receipt_ref, `External review receipt must be valid JSON: ${error.message}`);
|
|
701
|
+
return diagnostics;
|
|
702
|
+
}
|
|
703
|
+
validateExternalReceipt(receipt, evidence, diagnostics);
|
|
704
|
+
for (const [label, binding] of [
|
|
705
|
+
['receipt_contract', receipt?.reviewed_contract],
|
|
706
|
+
['receipt_authority', receipt?.reviewer_authority],
|
|
707
|
+
]) {
|
|
708
|
+
if (!exactKeys(binding, ['ref', 'sha256']) || !safeRepositoryRef(binding?.ref)) continue;
|
|
709
|
+
const bound = readRepositoryStableFile(repositoryRoot, binding.ref, label);
|
|
710
|
+
if (bound.failure) {
|
|
711
|
+
diagnostics.push(...bound.failure.diagnostics);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const boundDigest = crypto.createHash('sha256').update(bound.bytes).digest('hex');
|
|
715
|
+
if (boundDigest !== binding.sha256) {
|
|
716
|
+
diagnostic(diagnostics, `${label}_digest_mismatch`, binding.ref, `${label} digest does not match the observed repository bytes`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return diagnostics;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function projectionFor(value, evidencePath, evidenceBytes) {
|
|
723
|
+
const repositoryRoot = findRepositoryRoot(evidencePath);
|
|
724
|
+
const evidenceRef = repositoryRoot !== null
|
|
725
|
+
? path.relative(repositoryRoot, path.resolve(evidencePath)).split(path.sep).join('/')
|
|
726
|
+
: path.basename(evidencePath);
|
|
727
|
+
const evidenceDigest = crypto.createHash('sha256').update(evidenceBytes).digest('hex');
|
|
728
|
+
const base = {
|
|
729
|
+
task_id: value.task_id,
|
|
730
|
+
schema: 'ultra-task-evidence-v2',
|
|
731
|
+
evidence_ref: evidenceRef,
|
|
732
|
+
evidence_digest: evidenceDigest,
|
|
733
|
+
};
|
|
734
|
+
if ((value.task_review?.review_mode ?? 'strict-v4') === 'external-manual') {
|
|
735
|
+
return {
|
|
736
|
+
...base,
|
|
737
|
+
task_review_mode: 'external-manual',
|
|
738
|
+
task_review_receipt_ref: value.task_review.receipt_ref,
|
|
739
|
+
task_review_receipt_digest: value.task_review.receipt_sha256,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
...base,
|
|
744
|
+
task_review_session: value.task_review.session_id,
|
|
745
|
+
task_review_summary_digest: value.task_review.summary_digest,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function validateV2(value) {
|
|
750
|
+
const diagnostics = [];
|
|
751
|
+
if (!exactKeys(value, ROOT_KEYS)
|
|
752
|
+
|| !Object.keys(value).every((key, index) => key === ROOT_KEYS[index])) {
|
|
753
|
+
diagnostic(diagnostics, 'root_shape', '$', `Expected exact root fields: ${ROOT_KEYS.join(', ')}`);
|
|
754
|
+
return diagnostics;
|
|
755
|
+
}
|
|
756
|
+
const validTaskId = safeIdentifier(value.task_id);
|
|
757
|
+
if (!validTaskId) {
|
|
758
|
+
diagnostic(diagnostics, 'identity_shape', 'task_id', 'task_id must be one safe identifier component');
|
|
759
|
+
}
|
|
760
|
+
if (!safeIdentifier(value.change_id)) {
|
|
761
|
+
diagnostic(diagnostics, 'identity_shape', 'change_id', 'change_id must be one safe identifier component');
|
|
762
|
+
}
|
|
763
|
+
if (!exactKeys(value.context, ['path', 'acceptance_sha256'])
|
|
764
|
+
|| !nonempty(value.context?.path) || !digest(value.context?.acceptance_sha256, 64)) {
|
|
765
|
+
diagnostic(diagnostics, 'context_shape', 'context', 'Context requires only path and Acceptance-section SHA-256');
|
|
766
|
+
} else if (validTaskId
|
|
767
|
+
&& value.context.path !== `.ultra/contexts/task-${value.task_id}.md`) {
|
|
768
|
+
diagnostic(diagnostics, 'context_identity', 'context.path', 'Context path must exactly match the declared task_id');
|
|
769
|
+
}
|
|
770
|
+
validateFreshness(value.subject, diagnostics, 'subject');
|
|
771
|
+
validateAcceptance(value.acceptance, diagnostics);
|
|
772
|
+
validateDimensions(value.dimensions, diagnostics);
|
|
773
|
+
validateTaskReview(value.task_review, diagnostics, value.task_id);
|
|
774
|
+
if (!strings(value.artifacts, { nonemptyArray: true })) {
|
|
775
|
+
diagnostic(diagnostics, 'artifacts_shape', 'artifacts', 'Artifacts must be a non-empty string array');
|
|
776
|
+
}
|
|
777
|
+
if (!strings(value.limitations)) {
|
|
778
|
+
diagnostic(diagnostics, 'limitations_shape', 'limitations', 'Limitations must be a string array');
|
|
779
|
+
}
|
|
780
|
+
if (!timestamp(value.timestamp)) {
|
|
781
|
+
diagnostic(diagnostics, 'timestamp_shape', 'timestamp', 'Expected an RFC 3339 timestamp');
|
|
782
|
+
}
|
|
783
|
+
return diagnostics;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function main(argv) {
|
|
787
|
+
const verifyReceipt = argv.includes('--verify-external-receipt');
|
|
788
|
+
const wantProjection = argv.includes('--projection');
|
|
789
|
+
const inputs = argv.filter((argument) =>
|
|
790
|
+
argument !== '--verify-external-receipt' && argument !== '--projection');
|
|
791
|
+
if (inputs.length !== 1) {
|
|
792
|
+
process.stdout.write(`${JSON.stringify({
|
|
793
|
+
valid: false,
|
|
794
|
+
classification: 'invalid',
|
|
795
|
+
diagnostics: [{
|
|
796
|
+
code: 'usage',
|
|
797
|
+
at: '$',
|
|
798
|
+
message: 'usage: validate_task_evidence.cjs <evidence.json> [--verify-external-receipt] [--projection]',
|
|
799
|
+
}],
|
|
800
|
+
})}\n`);
|
|
801
|
+
return 1;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const snapshot = readBoundedStablePath(inputs[0]);
|
|
805
|
+
if (snapshot.failure) {
|
|
806
|
+
process.stdout.write(`${JSON.stringify(snapshot.failure)}\n`);
|
|
807
|
+
return 1;
|
|
808
|
+
}
|
|
809
|
+
const parsed = parseCapturedJson(snapshot);
|
|
810
|
+
if (parsed.failure) {
|
|
811
|
+
process.stdout.write(`${JSON.stringify(parsed.failure)}\n`);
|
|
812
|
+
return 1;
|
|
813
|
+
}
|
|
814
|
+
const { value } = parsed;
|
|
815
|
+
|
|
816
|
+
if (value?.$schema === 'ultra-task-evidence-v1') {
|
|
817
|
+
process.stdout.write(`${JSON.stringify({
|
|
818
|
+
valid: true,
|
|
819
|
+
classification: 'legacy-v1',
|
|
820
|
+
diagnostics: [{
|
|
821
|
+
code: 'legacy_evidence_v1',
|
|
822
|
+
at: '$schema',
|
|
823
|
+
message: 'Readable historical evidence; migrate before using it for a v2 completion claim',
|
|
824
|
+
}],
|
|
825
|
+
})}\n`);
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (value?.$schema !== 'ultra-task-evidence-v2') {
|
|
830
|
+
process.stdout.write(`${JSON.stringify({
|
|
831
|
+
valid: false,
|
|
832
|
+
classification: 'unknown',
|
|
833
|
+
diagnostics: [{ code: 'unknown_schema', at: '$schema', message: 'Unknown task evidence schema' }],
|
|
834
|
+
})}\n`);
|
|
835
|
+
return 1;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const diagnostics = validateV2(value);
|
|
839
|
+
// An external-manual record's receipt verification is mandatory on every
|
|
840
|
+
// canonical invocation; --verify-external-receipt stays a compatible alias.
|
|
841
|
+
const branch = value?.task_review?.review_mode ?? 'strict-v4';
|
|
842
|
+
if (diagnostics.length === 0 && branch === 'external-manual') {
|
|
843
|
+
diagnostics.push(...verifyExternalReceipt(inputs[0], value));
|
|
844
|
+
} else if (verifyReceipt && diagnostics.length === 0 && branch === 'strict-v4') {
|
|
845
|
+
diagnostics.push(...verifyExternalReceipt(inputs[0], value));
|
|
846
|
+
}
|
|
847
|
+
const output = {
|
|
848
|
+
valid: diagnostics.length === 0,
|
|
849
|
+
classification: 'current-v2',
|
|
850
|
+
diagnostics,
|
|
851
|
+
};
|
|
852
|
+
if (wantProjection && diagnostics.length === 0) {
|
|
853
|
+
output.projection = projectionFor(value, inputs[0], snapshot.bytes);
|
|
854
|
+
}
|
|
855
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
856
|
+
return diagnostics.length === 0 ? 0 : 1;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
process.exitCode = main(process.argv.slice(2));
|