ultra-builder-pro-cli 0.26.2 → 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 +35 -18
- package/.ultra-template/tasks.json +1 -0
- package/.ultra-template/test-report.json +17 -1
- package/CHANGELOG.md +163 -1
- package/README.md +108 -38
- 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 +84 -23
- package/docs/ARTIFACT-AUTHORITY.md +167 -22
- package/docs/DECISIONS.md +131 -17
- package/docs/PHILOSOPHY.md +58 -7
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
- package/docs/SKILL-AUTHORING.md +4 -3
- package/docs/WORKFLOW-LIFECYCLE.md +126 -28
- 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 +27 -4
- package/hooks/_common.py +1678 -61
- 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 +707 -22
- package/package.json +6 -3
- package/skills/ultra-change/SKILL.md +40 -9
- package/skills/ultra-change/references/change-contract.md +93 -27
- 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 +24 -2
- package/skills/ultra-delegate/references/delegation-contract.md +21 -1
- package/skills/ultra-deliver/SKILL.md +85 -11
- package/skills/ultra-dev/SKILL.md +171 -41
- package/skills/ultra-grilling/SKILL.md +10 -7
- package/skills/ultra-grilling/references/reframing.md +7 -3
- package/skills/ultra-init/SKILL.md +44 -7
- package/skills/ultra-init/scripts/init_project.cjs +968 -21
- package/skills/ultra-plan/SKILL.md +70 -44
- 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 +38 -10
- 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 +4 -3
- package/skills/ultra-research/references/99-synthesis.md +19 -2
- package/skills/ultra-research/references/north-star-v2.md +168 -0
- package/skills/ultra-research/references/wayfinding.md +8 -0
- package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
- package/skills/ultra-review/SKILL.md +201 -27
- 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 +95 -11
- package/skills/ultra-tdd/SKILL.md +11 -4
- package/skills/ultra-tdd/references/test-execution.md +6 -1
- package/skills/ultra-test/SKILL.md +145 -17
- package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +518 -30
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
9
|
+
|
|
10
|
+
const MAX_REVIEW_JSON_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
12
|
+
|
|
13
|
+
function diagnostic(code, message) {
|
|
14
|
+
return { code, message };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseObject(bytes, diagnostics, label) {
|
|
18
|
+
try {
|
|
19
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
20
|
+
const value = JSON.parse(text);
|
|
21
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
22
|
+
diagnostics.push(diagnostic(`${label}_not_object`, `${label} must be a JSON object`));
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
diagnostics.push(diagnostic(`${label}_unreadable`, `${label} is unreadable JSON: ${error.message}`));
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function reportLocation(reportFile, diagnostics) {
|
|
33
|
+
const reportDirectory = path.dirname(reportFile);
|
|
34
|
+
if (path.basename(reportFile) !== 'test-report.json'
|
|
35
|
+
|| path.basename(reportDirectory) !== '.ultra') {
|
|
36
|
+
diagnostics.push(diagnostic(
|
|
37
|
+
'report_canonical_path',
|
|
38
|
+
'report must be the canonical repository .ultra/test-report.json',
|
|
39
|
+
));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const inputProjectRoot = path.dirname(reportDirectory);
|
|
44
|
+
const root = fs.realpathSync(inputProjectRoot);
|
|
45
|
+
return {
|
|
46
|
+
projectRoot: root,
|
|
47
|
+
inputProjectRoot,
|
|
48
|
+
relative: '.ultra/test-report.json',
|
|
49
|
+
path: path.join(root, '.ultra', 'test-report.json'),
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
diagnostics.push(diagnostic(
|
|
53
|
+
'report_repository_root',
|
|
54
|
+
`report repository root cannot be resolved: ${error.message}`,
|
|
55
|
+
));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inspectRepositoryPath(projectRoot, relative, label) {
|
|
61
|
+
const segments = relative.split('/');
|
|
62
|
+
const directories = [projectRoot];
|
|
63
|
+
for (const segment of segments.slice(0, -1)) {
|
|
64
|
+
directories.push(path.join(directories[directories.length - 1], segment));
|
|
65
|
+
}
|
|
66
|
+
const componentIdentities = [];
|
|
67
|
+
const directoryFlags = fs.constants.O_RDONLY
|
|
68
|
+
| (fs.constants.O_DIRECTORY || 0)
|
|
69
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
70
|
+
for (let index = 0; index < directories.length; index += 1) {
|
|
71
|
+
const target = directories[index];
|
|
72
|
+
const component = index === 0 ? 'repository root' : segments[index - 1];
|
|
73
|
+
let entry;
|
|
74
|
+
let descriptor;
|
|
75
|
+
try {
|
|
76
|
+
entry = fs.lstatSync(target, { bigint: true });
|
|
77
|
+
if (entry.isSymbolicLink()) {
|
|
78
|
+
return {
|
|
79
|
+
error: diagnostic(
|
|
80
|
+
`${label}_snapshot_symlink_component`,
|
|
81
|
+
`${label} canonical path must not contain a symlink component: ${component}`,
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!entry.isDirectory()) {
|
|
86
|
+
return {
|
|
87
|
+
error: diagnostic(
|
|
88
|
+
`${label}_snapshot_not_regular`,
|
|
89
|
+
`${label} canonical path must end in a regular file beneath real directories; retry after workspace writes settle`,
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
descriptor = fs.openSync(target, directoryFlags);
|
|
94
|
+
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
95
|
+
if (!opened.isDirectory() || !sameComponentIdentity(entry, opened)) {
|
|
96
|
+
return {
|
|
97
|
+
error: diagnostic(
|
|
98
|
+
`${label}_snapshot_changed`,
|
|
99
|
+
`${label} path component changed before its stable raw-byte snapshot could be read; retry after writes settle`,
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
componentIdentities.push(componentIdentity(opened));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
error: diagnostic(
|
|
107
|
+
`${label}_snapshot_missing`,
|
|
108
|
+
`${label} canonical path is missing or unreadable: ${error.message}`,
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
} finally {
|
|
112
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const target = path.join(projectRoot, ...segments);
|
|
117
|
+
let entry;
|
|
118
|
+
try {
|
|
119
|
+
entry = fs.lstatSync(target, { bigint: true });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
error: diagnostic(
|
|
123
|
+
`${label}_snapshot_missing`,
|
|
124
|
+
`${label} canonical path is missing or unreadable: ${error.message}`,
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (entry.isSymbolicLink()) {
|
|
129
|
+
return {
|
|
130
|
+
error: diagnostic(
|
|
131
|
+
`${label}_snapshot_symlink_component`,
|
|
132
|
+
`${label} canonical path must not contain a symlink component: ${segments.at(-1)}`,
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!entry.isFile()) {
|
|
137
|
+
return {
|
|
138
|
+
error: diagnostic(
|
|
139
|
+
`${label}_snapshot_not_regular`,
|
|
140
|
+
`${label} canonical path must end in a regular file beneath real directories; retry after workspace writes settle`,
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { target, entry, componentIdentities };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function componentIdentity(value) {
|
|
148
|
+
return {
|
|
149
|
+
dev: value.dev,
|
|
150
|
+
ino: value.ino,
|
|
151
|
+
mode: value.mode,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sameComponentIdentity(left, right) {
|
|
156
|
+
return left.dev === right.dev
|
|
157
|
+
&& left.ino === right.ino
|
|
158
|
+
&& left.mode === right.mode;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sameComponentIdentities(left, right) {
|
|
162
|
+
return left.length === right.length
|
|
163
|
+
&& left.every((identity, index) => sameComponentIdentity(identity, right[index]));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sameFileIdentity(left, right) {
|
|
167
|
+
return left.dev === right.dev
|
|
168
|
+
&& left.ino === right.ino
|
|
169
|
+
&& left.mode === right.mode
|
|
170
|
+
&& left.size === right.size
|
|
171
|
+
&& left.mtimeNs === right.mtimeNs
|
|
172
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readBoundedStableSnapshot(projectRoot, relative, diagnostics, label) {
|
|
176
|
+
const beforePath = inspectRepositoryPath(projectRoot, relative, label);
|
|
177
|
+
if (beforePath.error) {
|
|
178
|
+
diagnostics.push(beforePath.error);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (beforePath.entry.size > BigInt(MAX_REVIEW_JSON_BYTES)) {
|
|
182
|
+
diagnostics.push(diagnostic(
|
|
183
|
+
`${label}_snapshot_oversize`,
|
|
184
|
+
`${label} exceeds the ${MAX_REVIEW_JSON_BYTES}-byte snapshot limit`,
|
|
185
|
+
));
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let descriptor;
|
|
190
|
+
const chunks = [];
|
|
191
|
+
let observed = 0;
|
|
192
|
+
let beforeDescriptor;
|
|
193
|
+
let afterDescriptor;
|
|
194
|
+
try {
|
|
195
|
+
descriptor = fs.openSync(
|
|
196
|
+
beforePath.target,
|
|
197
|
+
fs.constants.O_RDONLY
|
|
198
|
+
| fs.constants.O_NONBLOCK
|
|
199
|
+
| (fs.constants.O_NOFOLLOW || 0),
|
|
200
|
+
);
|
|
201
|
+
beforeDescriptor = fs.fstatSync(descriptor, { bigint: true });
|
|
202
|
+
if (!beforeDescriptor.isFile()
|
|
203
|
+
|| !sameFileIdentity(beforePath.entry, beforeDescriptor)) {
|
|
204
|
+
diagnostics.push(diagnostic(
|
|
205
|
+
`${label}_snapshot_changed`,
|
|
206
|
+
`${label} changed before its stable raw-byte snapshot could be read; retry after writes settle`,
|
|
207
|
+
));
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
const capacity = Math.min(
|
|
213
|
+
READ_CHUNK_BYTES,
|
|
214
|
+
MAX_REVIEW_JSON_BYTES + 1 - observed,
|
|
215
|
+
);
|
|
216
|
+
const chunk = Buffer.allocUnsafe(capacity);
|
|
217
|
+
const count = fs.readSync(descriptor, chunk, 0, capacity, null);
|
|
218
|
+
if (count === 0) break;
|
|
219
|
+
observed += count;
|
|
220
|
+
if (observed > MAX_REVIEW_JSON_BYTES) {
|
|
221
|
+
diagnostics.push(diagnostic(
|
|
222
|
+
`${label}_snapshot_oversize`,
|
|
223
|
+
`${label} exceeds the ${MAX_REVIEW_JSON_BYTES}-byte snapshot limit`,
|
|
224
|
+
));
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
chunks.push(chunk.subarray(0, count));
|
|
228
|
+
}
|
|
229
|
+
afterDescriptor = fs.fstatSync(descriptor, { bigint: true });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
diagnostics.push(diagnostic(
|
|
232
|
+
`${label}_snapshot_unreadable`,
|
|
233
|
+
`${label} raw-byte snapshot could not be captured safely; retry after writes settle: ${error.message}`,
|
|
234
|
+
));
|
|
235
|
+
return null;
|
|
236
|
+
} finally {
|
|
237
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const afterPath = inspectRepositoryPath(projectRoot, relative, label);
|
|
241
|
+
if (afterPath.error
|
|
242
|
+
|| !sameComponentIdentities(
|
|
243
|
+
beforePath.componentIdentities,
|
|
244
|
+
afterPath.componentIdentities,
|
|
245
|
+
)
|
|
246
|
+
|| !sameFileIdentity(beforeDescriptor, afterDescriptor)
|
|
247
|
+
|| !sameFileIdentity(afterDescriptor, afterPath.entry)
|
|
248
|
+
|| BigInt(observed) !== afterDescriptor.size) {
|
|
249
|
+
diagnostics.push(diagnostic(
|
|
250
|
+
`${label}_snapshot_changed`,
|
|
251
|
+
`${label} changed while its stable raw-byte snapshot was captured; retry after writes settle`,
|
|
252
|
+
));
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
path: beforePath.target,
|
|
257
|
+
bytes: Buffer.concat(chunks, observed),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function canonicalSummaryPath(location, summaryFile, value, diagnostics) {
|
|
262
|
+
if (!location || typeof value !== 'string') {
|
|
263
|
+
diagnostics.push(diagnostic(
|
|
264
|
+
'review_summary_ref',
|
|
265
|
+
'report review.summary_ref must be a normalized repository-relative summary path',
|
|
266
|
+
));
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const segments = value.split('/');
|
|
270
|
+
const canonical = segments.length === 4
|
|
271
|
+
&& segments[0] === '.ultra'
|
|
272
|
+
&& segments[1] === 'reviews'
|
|
273
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segments[2])
|
|
274
|
+
&& segments[3] === 'SUMMARY.json'
|
|
275
|
+
&& !value.includes('\\')
|
|
276
|
+
&& !value.includes('\0')
|
|
277
|
+
&& path.posix.normalize(value) === value;
|
|
278
|
+
if (!canonical) {
|
|
279
|
+
diagnostics.push(diagnostic(
|
|
280
|
+
'review_summary_ref',
|
|
281
|
+
'report review.summary_ref must be .ultra/reviews/<session>/SUMMARY.json as a normalized repository-relative POSIX path',
|
|
282
|
+
));
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const candidate = path.join(location.projectRoot, ...segments);
|
|
287
|
+
const supplied = path.resolve(summaryFile);
|
|
288
|
+
const inputCandidate = path.join(location.inputProjectRoot, ...segments);
|
|
289
|
+
if (supplied !== candidate && supplied !== inputCandidate) {
|
|
290
|
+
diagnostics.push(diagnostic(
|
|
291
|
+
'review_summary_ref',
|
|
292
|
+
'the CLI summary path must equal the report repository summary_ref',
|
|
293
|
+
));
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
path: candidate,
|
|
298
|
+
relative: value,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function validateSummaryWithWaiter(
|
|
303
|
+
summaryFile,
|
|
304
|
+
summary,
|
|
305
|
+
snapshot,
|
|
306
|
+
diagnostics,
|
|
307
|
+
legacyV4,
|
|
308
|
+
) {
|
|
309
|
+
if (!summary || typeof summary.packet_digest !== 'string'
|
|
310
|
+
|| !/^[0-9a-f]{64}$/.test(summary.packet_digest)) {
|
|
311
|
+
diagnostics.push(diagnostic(
|
|
312
|
+
'summary_packet_digest',
|
|
313
|
+
'summary packet_digest must be a lowercase SHA-256 before waiter validation',
|
|
314
|
+
));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const waiter = path.resolve(
|
|
318
|
+
__dirname,
|
|
319
|
+
'..',
|
|
320
|
+
'..',
|
|
321
|
+
'ultra-review',
|
|
322
|
+
'scripts',
|
|
323
|
+
'review_wait.py',
|
|
324
|
+
);
|
|
325
|
+
const waiterArguments = [
|
|
326
|
+
waiter,
|
|
327
|
+
path.dirname(summaryFile),
|
|
328
|
+
'summary',
|
|
329
|
+
'--packet-digest',
|
|
330
|
+
summary.packet_digest,
|
|
331
|
+
];
|
|
332
|
+
if (legacyV4) waiterArguments.push('--legacy-v4');
|
|
333
|
+
waiterArguments.push(
|
|
334
|
+
'--summary-snapshot-digest',
|
|
335
|
+
snapshot.digest,
|
|
336
|
+
);
|
|
337
|
+
const result = spawnSync('python3', waiterArguments, {
|
|
338
|
+
encoding: 'utf8',
|
|
339
|
+
input: snapshot.bytes,
|
|
340
|
+
timeout: 10_000,
|
|
341
|
+
maxBuffer: 64 * 1024,
|
|
342
|
+
env: {
|
|
343
|
+
...process.env,
|
|
344
|
+
UBP_REVIEW_WAIT_TIMEOUT: '0.05',
|
|
345
|
+
UBP_REVIEW_WAIT_POLL: '0.01',
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
if (result.error || result.status !== 0) {
|
|
349
|
+
const detail = result.error?.message
|
|
350
|
+
|| result.stdout?.trim()
|
|
351
|
+
|| result.stderr?.trim()
|
|
352
|
+
|| `exit ${result.status}`;
|
|
353
|
+
diagnostics.push(diagnostic(
|
|
354
|
+
'summary_waiter_validation',
|
|
355
|
+
`summary failed canonical review_wait.py validation: ${detail}`,
|
|
356
|
+
));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const receipt = JSON.parse(result.stdout);
|
|
361
|
+
if (receipt.status !== 'complete' || receipt.summary_digest !== snapshot.digest) {
|
|
362
|
+
diagnostics.push(diagnostic(
|
|
363
|
+
'summary_waiter_snapshot',
|
|
364
|
+
'canonical review_wait.py did not validate the exact supplied SUMMARY.json snapshot',
|
|
365
|
+
));
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
diagnostics.push(diagnostic(
|
|
369
|
+
'summary_waiter_snapshot',
|
|
370
|
+
`canonical review_wait.py returned an invalid snapshot receipt: ${error.message}`,
|
|
371
|
+
));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function validateTransport(summaryFile, reportFile, options = {}) {
|
|
376
|
+
const resolvedReport = path.resolve(reportFile);
|
|
377
|
+
const diagnostics = [];
|
|
378
|
+
const location = reportLocation(resolvedReport, diagnostics);
|
|
379
|
+
const reportSnapshot = location
|
|
380
|
+
? readBoundedStableSnapshot(
|
|
381
|
+
location.projectRoot,
|
|
382
|
+
location.relative,
|
|
383
|
+
diagnostics,
|
|
384
|
+
'report',
|
|
385
|
+
)
|
|
386
|
+
: null;
|
|
387
|
+
const report = reportSnapshot
|
|
388
|
+
? parseObject(reportSnapshot.bytes, diagnostics, 'report')
|
|
389
|
+
: null;
|
|
390
|
+
const review = report && report.review;
|
|
391
|
+
const summaryLocation = report
|
|
392
|
+
? canonicalSummaryPath(
|
|
393
|
+
location,
|
|
394
|
+
summaryFile,
|
|
395
|
+
review && typeof review === 'object' && !Array.isArray(review)
|
|
396
|
+
? review.summary_ref
|
|
397
|
+
: null,
|
|
398
|
+
diagnostics,
|
|
399
|
+
)
|
|
400
|
+
: null;
|
|
401
|
+
const summarySnapshot = summaryLocation
|
|
402
|
+
? readBoundedStableSnapshot(
|
|
403
|
+
location.projectRoot,
|
|
404
|
+
summaryLocation.relative,
|
|
405
|
+
diagnostics,
|
|
406
|
+
'summary',
|
|
407
|
+
)
|
|
408
|
+
: null;
|
|
409
|
+
const summary = summarySnapshot
|
|
410
|
+
? parseObject(summarySnapshot.bytes, diagnostics, 'summary')
|
|
411
|
+
: null;
|
|
412
|
+
const summaryDigest = summarySnapshot
|
|
413
|
+
? crypto.createHash('sha256').update(summarySnapshot.bytes).digest('hex')
|
|
414
|
+
: null;
|
|
415
|
+
if (summaryLocation && summarySnapshot) {
|
|
416
|
+
validateSummaryWithWaiter(
|
|
417
|
+
summaryLocation.path,
|
|
418
|
+
summary,
|
|
419
|
+
{ bytes: summarySnapshot.bytes, digest: summaryDigest },
|
|
420
|
+
diagnostics,
|
|
421
|
+
options.legacyV4 === true,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (summary && summary.$schema !== 'ultra-review-summary-v4') {
|
|
426
|
+
diagnostics.push(diagnostic(
|
|
427
|
+
'summary_schema',
|
|
428
|
+
'summary must use ultra-review-summary-v4',
|
|
429
|
+
));
|
|
430
|
+
}
|
|
431
|
+
if (report && !['ultra-test-report-v1', 'ultra-test-report-v2'].includes(report.$schema)) {
|
|
432
|
+
diagnostics.push(diagnostic(
|
|
433
|
+
'report_schema',
|
|
434
|
+
'report must use ultra-test-report-v1 or ultra-test-report-v2',
|
|
435
|
+
));
|
|
436
|
+
}
|
|
437
|
+
if (report && report.$schema === 'ultra-test-report-v2' && !Array.isArray(report.task_evidence)) {
|
|
438
|
+
diagnostics.push(diagnostic(
|
|
439
|
+
'report_task_evidence',
|
|
440
|
+
'ultra-test-report-v2 task_evidence must be an array',
|
|
441
|
+
));
|
|
442
|
+
}
|
|
443
|
+
if (summary && !Array.isArray(summary.findings)) {
|
|
444
|
+
diagnostics.push(diagnostic('summary_findings', 'summary findings must be an array'));
|
|
445
|
+
}
|
|
446
|
+
if (report && !Array.isArray(report.findings)) {
|
|
447
|
+
diagnostics.push(diagnostic('report_findings', 'report findings must be an array'));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (report && (!review || Array.isArray(review) || typeof review !== 'object')) {
|
|
451
|
+
diagnostics.push(diagnostic('report_review', 'report review must be an object'));
|
|
452
|
+
}
|
|
453
|
+
if (summary && review && typeof review === 'object' && !Array.isArray(review)) {
|
|
454
|
+
const exactMetadata = [
|
|
455
|
+
['session', summary.session],
|
|
456
|
+
['packet_digest', summary.packet_digest],
|
|
457
|
+
['execution_mode', summary.execution_mode],
|
|
458
|
+
['verdict', summary.verdict],
|
|
459
|
+
['context_digest', summary.context_digest],
|
|
460
|
+
['worktree_digest', summary.worktree_digest],
|
|
461
|
+
];
|
|
462
|
+
if (options.legacyV4 !== true) {
|
|
463
|
+
exactMetadata.push(
|
|
464
|
+
['admission_digest', summary.admission_digest],
|
|
465
|
+
['subject_digest', summary.subject_digest],
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
for (const [field, expected] of exactMetadata) {
|
|
469
|
+
if (review[field] !== expected) {
|
|
470
|
+
diagnostics.push(diagnostic(
|
|
471
|
+
`review_${field}`,
|
|
472
|
+
`report review.${field} must equal SUMMARY.json ${field}`,
|
|
473
|
+
));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (review.finding_schema !== 'ultra-review-findings-v4') {
|
|
477
|
+
diagnostics.push(diagnostic(
|
|
478
|
+
'review_finding_schema',
|
|
479
|
+
'report review.finding_schema must be ultra-review-findings-v4',
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
if (review.summary_digest !== summaryDigest) {
|
|
483
|
+
diagnostics.push(diagnostic(
|
|
484
|
+
'review_summary_digest',
|
|
485
|
+
'report review.summary_digest must equal the SHA-256 of exact SUMMARY.json bytes',
|
|
486
|
+
));
|
|
487
|
+
}
|
|
488
|
+
for (const field of ['coverage_refs', 'limitations']) {
|
|
489
|
+
if (!isDeepStrictEqual(review[field], summary[field])) {
|
|
490
|
+
diagnostics.push(diagnostic(
|
|
491
|
+
`review_${field}`,
|
|
492
|
+
`report review.${field} must equal SUMMARY.json ${field} unchanged`,
|
|
493
|
+
));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (summary && report) {
|
|
498
|
+
const exactSubjects = [
|
|
499
|
+
['change_id', report.change_id, summary.change_id],
|
|
500
|
+
['task_ids', report.task_ids, summary.task_ids],
|
|
501
|
+
['git_commit', report.git_commit, summary.head],
|
|
502
|
+
];
|
|
503
|
+
for (const [field, observed, expected] of exactSubjects) {
|
|
504
|
+
if (!isDeepStrictEqual(observed, expected)) {
|
|
505
|
+
diagnostics.push(diagnostic(
|
|
506
|
+
`review_subject_${field}`,
|
|
507
|
+
`report ${field} must equal SUMMARY.json ${field === 'git_commit' ? 'head' : field}`,
|
|
508
|
+
));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (summary && report && Array.isArray(summary.findings) && Array.isArray(report.findings)
|
|
513
|
+
&& !isDeepStrictEqual(report.findings, summary.findings)) {
|
|
514
|
+
diagnostics.push(diagnostic(
|
|
515
|
+
'finding_transport_mismatch',
|
|
516
|
+
'report findings must equal every SUMMARY.json finding object in order and unchanged',
|
|
517
|
+
));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
$schema: 'ultra-review-transport-validation-v1',
|
|
522
|
+
summary: summaryLocation?.path || path.resolve(summaryFile),
|
|
523
|
+
report: resolvedReport,
|
|
524
|
+
summary_digest: summaryDigest,
|
|
525
|
+
finding_count: Array.isArray(summary?.findings) ? summary.findings.length : 0,
|
|
526
|
+
valid: diagnostics.length === 0,
|
|
527
|
+
diagnostics,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function main(argv) {
|
|
532
|
+
const legacyV4 = argv.length === 5 && argv[4] === '--legacy-v4';
|
|
533
|
+
if ((argv.length !== 4 && !legacyV4)
|
|
534
|
+
|| argv[0] !== '--summary'
|
|
535
|
+
|| argv[2] !== '--report') {
|
|
536
|
+
process.stderr.write(
|
|
537
|
+
'usage: validate_review_transport.cjs --summary <SUMMARY.json> --report <test-report.json> [--legacy-v4]\n',
|
|
538
|
+
);
|
|
539
|
+
process.exitCode = 2;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const result = validateTransport(argv[1], argv[3], { legacyV4 });
|
|
543
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
544
|
+
if (!result.valid) process.exitCode = 1;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (require.main === module) main(process.argv.slice(2));
|
|
548
|
+
|
|
549
|
+
module.exports = { validateTransport };
|