@open-agent-toolkit/cli 0.2.52 → 0.2.54
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/assets/agents/recon-worker.md +105 -0
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/tool-packs.md +62 -8
- package/assets/docs/contributing/skills.md +26 -0
- package/assets/docs/workflows/skills/index.md +3 -0
- package/assets/docs/workflows/skills/recon.md +194 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +105 -4
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +226 -0
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/recon/SKILL.md +260 -0
- package/assets/skills/recon/references/packet-contract.md +307 -0
- package/assets/skills/recon/references/profiles.md +58 -0
- package/assets/skills/recon/references/worker-contract.md +71 -0
- package/assets/skills/recon/scripts/create-review-brief.mjs +452 -0
- package/assets/skills/recon/scripts/lib/canonical-json.mjs +55 -0
- package/assets/skills/recon/scripts/lib/contracts.mjs +2462 -0
- package/assets/skills/recon/scripts/lib/safe-path.mjs +120 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +86 -0
- package/assets/skills/recon/scripts/reconcile-ledger.mjs +360 -0
- package/assets/skills/recon/scripts/render-packet.mjs +392 -0
- package/assets/skills/recon/scripts/validate-artifact.mjs +96 -0
- package/assets/skills/recon/scripts/validate-packet.mjs +2359 -0
- package/dist/commands/doctor/index.js +5 -5
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +7 -8
- package/dist/commands/status/index.d.ts +2 -2
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +8 -8
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +25 -9
- package/dist/commands/sync/sync.types.d.ts +2 -2
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/commands/tools/migrate/index.d.ts.map +1 -1
- package/dist/commands/tools/migrate/index.js +25 -1
- package/dist/commands/tools/migrate/migrate-pack.d.ts +5 -0
- package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -1
- package/dist/commands/tools/migrate/migrate-pack.js +66 -7
- package/dist/commands/tools/remove/index.js +1 -1
- package/dist/commands/tools/remove/remove-tools.d.ts +5 -1
- package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
- package/dist/commands/tools/remove/remove-tools.js +81 -1
- package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +3 -0
- package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -1
- package/dist/commands/tools/shared/apply-pack-reconcile.js +40 -9
- package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/format-pack-inventory.js +3 -0
- package/dist/commands/tools/shared/install-sync-context.d.ts +2 -1
- package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
- package/dist/commands/tools/shared/install-sync-context.js +14 -3
- package/dist/commands/tools/shared/pack-dependencies.d.ts +14 -0
- package/dist/commands/tools/shared/pack-dependencies.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-dependencies.js +54 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-inventory.js +9 -9
- package/dist/commands/tools/shared/pack-lifecycle.d.ts +9 -1
- package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-lifecycle.js +102 -9
- package/dist/commands/tools/shared/pack-manifest.d.ts +2 -2
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +91 -2
- package/dist/commands/tools/shared/pack-reconcile.d.ts +15 -1
- package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-reconcile.js +75 -13
- package/dist/commands/tools/shared/scoped-pack-intent.d.ts +8 -0
- package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -1
- package/dist/commands/tools/shared/scoped-pack-intent.js +61 -15
- package/dist/commands/tools/shared/types.d.ts +7 -0
- package/dist/commands/tools/shared/types.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.js +7 -1
- package/dist/config/oat-config.d.ts +5 -1
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +19 -0
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/scanner.d.ts +16 -1
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +98 -4
- package/dist/providers/codex/codec/sync-extension.d.ts +1 -1
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +7 -7
- package/dist/providers/cursor/codec/sync-extension.d.ts +1 -1
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +11 -13
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
|
|
8
|
+
import { issue, isObject, validateArtifactShape } from './lib/contracts.mjs';
|
|
9
|
+
import { assertSafeOutputPath } from './lib/safe-path.mjs';
|
|
10
|
+
|
|
11
|
+
const commonKeys = [
|
|
12
|
+
'kind',
|
|
13
|
+
'schemaVersion',
|
|
14
|
+
'id',
|
|
15
|
+
'runId',
|
|
16
|
+
'mode',
|
|
17
|
+
'createdAt',
|
|
18
|
+
'excludedInputs',
|
|
19
|
+
];
|
|
20
|
+
const allowedKeys = {
|
|
21
|
+
verify: new Set([...commonKeys, 'claims', 'sources']),
|
|
22
|
+
adversary: new Set([
|
|
23
|
+
...commonKeys,
|
|
24
|
+
'scope',
|
|
25
|
+
'questions',
|
|
26
|
+
'provisionalStatements',
|
|
27
|
+
]),
|
|
28
|
+
coverage: new Set([...commonKeys, 'scope', 'questions', 'claims']),
|
|
29
|
+
};
|
|
30
|
+
const forbiddenKeys = new Set([
|
|
31
|
+
'derivedFrom',
|
|
32
|
+
'provenance',
|
|
33
|
+
'reviewIds',
|
|
34
|
+
'synthesis',
|
|
35
|
+
'compilerReasoning',
|
|
36
|
+
'inputArtifacts',
|
|
37
|
+
'priorReviews',
|
|
38
|
+
'dossierPath',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const reviewSourceFields = {
|
|
42
|
+
repository: ['root', 'revision', 'dirty', 'contentHashes'],
|
|
43
|
+
file: ['path', 'contentHash'],
|
|
44
|
+
url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
|
|
45
|
+
'command-output': [
|
|
46
|
+
'argv',
|
|
47
|
+
'cwd',
|
|
48
|
+
'exitStatus',
|
|
49
|
+
'outputPath',
|
|
50
|
+
'outputDigest',
|
|
51
|
+
'environmentNames',
|
|
52
|
+
],
|
|
53
|
+
'connected-resource': [
|
|
54
|
+
'system',
|
|
55
|
+
'resourceId',
|
|
56
|
+
'resourceVersion',
|
|
57
|
+
'retrievalToken',
|
|
58
|
+
'capturePath',
|
|
59
|
+
'captureDigest',
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const commonReviewSourceFields = [
|
|
63
|
+
'id',
|
|
64
|
+
'kind',
|
|
65
|
+
'available',
|
|
66
|
+
'authority',
|
|
67
|
+
'observedAt',
|
|
68
|
+
'validationState',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function selectedClaims(ledger, claimIds) {
|
|
72
|
+
const selected = claimIds ? new Set(claimIds) : null;
|
|
73
|
+
return ledger.claims.filter((claim) => !selected || selected.has(claim.id));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function projectReviewSource(source) {
|
|
77
|
+
const kindFields = reviewSourceFields[source.kind];
|
|
78
|
+
if (!kindFields) {
|
|
79
|
+
throw new Error(`Cannot project unknown source kind ${source.kind}`);
|
|
80
|
+
}
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
[...commonReviewSourceFields, ...kindFields]
|
|
83
|
+
.filter((key) => Object.hasOwn(source, key))
|
|
84
|
+
.map((key) => [key, structuredClone(source[key])]),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verificationBrief(input) {
|
|
89
|
+
const claims = selectedClaims(input.ledger, input.claimIds);
|
|
90
|
+
const evidenceById = new Map(
|
|
91
|
+
input.ledger.evidence.map((evidence) => [evidence.id, evidence]),
|
|
92
|
+
);
|
|
93
|
+
const sourceIds = new Set();
|
|
94
|
+
const projectedClaims = claims.map((claim) => ({
|
|
95
|
+
id: claim.id,
|
|
96
|
+
statement: claim.statement,
|
|
97
|
+
evidence: claim.evidence.map((link) => {
|
|
98
|
+
const evidence = evidenceById.get(link.evidenceId);
|
|
99
|
+
if (!evidence) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Claim ${claim.id} references missing evidence ${link.evidenceId}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
sourceIds.add(evidence.sourceId);
|
|
105
|
+
return {
|
|
106
|
+
id: evidence.id,
|
|
107
|
+
sourceId: evidence.sourceId,
|
|
108
|
+
displayExcerpt: evidence.displayExcerpt,
|
|
109
|
+
locator: structuredClone(evidence.locator),
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
112
|
+
}));
|
|
113
|
+
const declaredSourceIds = new Set(
|
|
114
|
+
input.manifest.sources.map((source) => source.id),
|
|
115
|
+
);
|
|
116
|
+
for (const sourceId of sourceIds) {
|
|
117
|
+
if (!declaredSourceIds.has(sourceId)) {
|
|
118
|
+
throw new Error(`Evidence references missing source ${sourceId}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
kind: 'recon.review-brief',
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
id: input.id,
|
|
125
|
+
runId: input.manifest.run.id,
|
|
126
|
+
mode: 'verify',
|
|
127
|
+
createdAt: input.createdAt,
|
|
128
|
+
excludedInputs: [
|
|
129
|
+
'worker_intermediates',
|
|
130
|
+
'prior_reasoning',
|
|
131
|
+
'consumer_summary',
|
|
132
|
+
'artifact_lineage',
|
|
133
|
+
'earlier_reviews',
|
|
134
|
+
],
|
|
135
|
+
claims: projectedClaims,
|
|
136
|
+
sources: input.manifest.sources
|
|
137
|
+
.filter((source) => sourceIds.has(source.id))
|
|
138
|
+
.map(projectReviewSource),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function adversarialBrief(input) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'recon.review-brief',
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
id: input.id,
|
|
147
|
+
runId: input.manifest.run.id,
|
|
148
|
+
mode: 'adversary',
|
|
149
|
+
createdAt: input.createdAt,
|
|
150
|
+
excludedInputs: [
|
|
151
|
+
'worker_intermediates',
|
|
152
|
+
'prior_reasoning',
|
|
153
|
+
'consumer_summary',
|
|
154
|
+
'artifact_lineage',
|
|
155
|
+
'earlier_reviews',
|
|
156
|
+
'verification_conclusions',
|
|
157
|
+
],
|
|
158
|
+
scope: {
|
|
159
|
+
included: structuredClone(input.manifest.request.includedScope ?? []),
|
|
160
|
+
excluded: structuredClone(input.manifest.request.excludedScope ?? []),
|
|
161
|
+
},
|
|
162
|
+
questions: structuredClone(input.manifest.request.questions ?? []),
|
|
163
|
+
provisionalStatements: selectedClaims(input.ledger, input.claimIds).map(
|
|
164
|
+
(claim) => ({ id: claim.id, statement: claim.statement }),
|
|
165
|
+
),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function coverageBrief(input) {
|
|
170
|
+
return {
|
|
171
|
+
kind: 'recon.review-brief',
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
id: input.id,
|
|
174
|
+
runId: input.manifest.run.id,
|
|
175
|
+
mode: 'coverage',
|
|
176
|
+
createdAt: input.createdAt,
|
|
177
|
+
excludedInputs: [
|
|
178
|
+
'worker_intermediates',
|
|
179
|
+
'prior_reasoning',
|
|
180
|
+
'consumer_summary',
|
|
181
|
+
'artifact_lineage',
|
|
182
|
+
'earlier_reviews',
|
|
183
|
+
'verification_conclusions',
|
|
184
|
+
'adversarial_conclusions',
|
|
185
|
+
],
|
|
186
|
+
scope: {
|
|
187
|
+
included: structuredClone(input.manifest.request.includedScope ?? []),
|
|
188
|
+
excluded: structuredClone(input.manifest.request.excludedScope ?? []),
|
|
189
|
+
},
|
|
190
|
+
questions: structuredClone(input.manifest.request.questions ?? []),
|
|
191
|
+
claims: selectedClaims(input.ledger, input.claimIds).map((claim) => ({
|
|
192
|
+
id: claim.id,
|
|
193
|
+
statement: claim.statement,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function createReviewBrief(input) {
|
|
199
|
+
if (!['verify', 'adversary', 'coverage'].includes(input.mode)) {
|
|
200
|
+
throw new Error(`Unsupported review brief mode: ${input.mode}`);
|
|
201
|
+
}
|
|
202
|
+
if (!input.id || !input.createdAt || !input.manifest || !input.ledger) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'Review brief requires id, createdAt, manifest, and ledger',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const brief =
|
|
208
|
+
input.mode === 'verify'
|
|
209
|
+
? verificationBrief(input)
|
|
210
|
+
: input.mode === 'adversary'
|
|
211
|
+
? adversarialBrief(input)
|
|
212
|
+
: coverageBrief(input);
|
|
213
|
+
const result = validateReviewBrief(brief);
|
|
214
|
+
if (!result.valid) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Review brief violates selective blindness: ${result.errors[0].message}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return brief;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function inspectForbidden(value, path, errors) {
|
|
223
|
+
if (typeof value === 'string') {
|
|
224
|
+
if (/raw[\\/]dossiers/i.test(value)) {
|
|
225
|
+
errors.push(
|
|
226
|
+
issue('BLINDNESS_VIOLATION', 'Raw dossier paths are forbidden', path),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
value.forEach((item, index) =>
|
|
233
|
+
inspectForbidden(item, `${path}[${index}]`, errors),
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!isObject(value)) return;
|
|
238
|
+
for (const [key, item] of Object.entries(value)) {
|
|
239
|
+
if (forbiddenKeys.has(key)) {
|
|
240
|
+
errors.push(
|
|
241
|
+
issue(
|
|
242
|
+
'BLINDNESS_VIOLATION',
|
|
243
|
+
`Forbidden field ${key}`,
|
|
244
|
+
`${path}.${key}`,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
inspectForbidden(item, `${path}.${key}`, errors);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function validateReviewBrief(brief) {
|
|
253
|
+
const errors = [];
|
|
254
|
+
if (
|
|
255
|
+
!isObject(brief) ||
|
|
256
|
+
brief.kind !== 'recon.review-brief' ||
|
|
257
|
+
brief.schemaVersion !== 1
|
|
258
|
+
) {
|
|
259
|
+
errors.push(
|
|
260
|
+
issue('INVALID_REVIEW_BRIEF', 'Expected recon.review-brief version 1'),
|
|
261
|
+
);
|
|
262
|
+
return { valid: false, errors };
|
|
263
|
+
}
|
|
264
|
+
const allowed = allowedKeys[brief.mode];
|
|
265
|
+
if (!allowed) {
|
|
266
|
+
errors.push(
|
|
267
|
+
issue(
|
|
268
|
+
'INVALID_REVIEW_MODE',
|
|
269
|
+
'Review mode must be verify, adversary, or coverage',
|
|
270
|
+
'$.mode',
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
return { valid: false, errors };
|
|
274
|
+
}
|
|
275
|
+
for (const key of Object.keys(brief)) {
|
|
276
|
+
if (!allowed.has(key)) {
|
|
277
|
+
errors.push(
|
|
278
|
+
issue(
|
|
279
|
+
'BLINDNESS_VIOLATION',
|
|
280
|
+
`Unexpected top-level field ${key}`,
|
|
281
|
+
`$.${key}`,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
inspectForbidden(brief, '$', errors);
|
|
287
|
+
if (brief.mode === 'verify') {
|
|
288
|
+
if (!Array.isArray(brief.claims) || !Array.isArray(brief.sources)) {
|
|
289
|
+
errors.push(
|
|
290
|
+
issue(
|
|
291
|
+
'INVALID_REVIEW_BRIEF',
|
|
292
|
+
'Verification brief needs claims and sources',
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
for (const [index, claim] of (brief.claims ?? []).entries()) {
|
|
297
|
+
const keys = Object.keys(claim).sort();
|
|
298
|
+
if (keys.join(',') !== 'evidence,id,statement') {
|
|
299
|
+
errors.push(
|
|
300
|
+
issue(
|
|
301
|
+
'BLINDNESS_VIOLATION',
|
|
302
|
+
'Verification claims have an invalid projection',
|
|
303
|
+
`$.claims[${index}]`,
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
for (const [evidenceIndex, evidence] of (
|
|
308
|
+
claim.evidence ?? []
|
|
309
|
+
).entries()) {
|
|
310
|
+
const evidenceKeys = Object.keys(evidence).sort();
|
|
311
|
+
if (evidenceKeys.join(',') !== 'displayExcerpt,id,locator,sourceId') {
|
|
312
|
+
errors.push(
|
|
313
|
+
issue(
|
|
314
|
+
'BLINDNESS_VIOLATION',
|
|
315
|
+
'Verification evidence has an invalid projection',
|
|
316
|
+
`$.claims[${index}].evidence[${evidenceIndex}]`,
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else if (brief.mode === 'adversary') {
|
|
323
|
+
if (
|
|
324
|
+
!Array.isArray(brief.questions) ||
|
|
325
|
+
!Array.isArray(brief.provisionalStatements) ||
|
|
326
|
+
!isObject(brief.scope)
|
|
327
|
+
) {
|
|
328
|
+
errors.push(
|
|
329
|
+
issue(
|
|
330
|
+
'INVALID_REVIEW_BRIEF',
|
|
331
|
+
'Adversarial brief needs scope, questions, and provisional statements',
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
for (const [index, statement] of (
|
|
336
|
+
brief.provisionalStatements ?? []
|
|
337
|
+
).entries()) {
|
|
338
|
+
if (
|
|
339
|
+
!isObject(statement) ||
|
|
340
|
+
Object.keys(statement).sort().join(',') !== 'id,statement'
|
|
341
|
+
) {
|
|
342
|
+
errors.push(
|
|
343
|
+
issue(
|
|
344
|
+
'BLINDNESS_VIOLATION',
|
|
345
|
+
'Adversarial statements have an invalid projection',
|
|
346
|
+
`$.provisionalStatements[${index}]`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else if (brief.mode === 'coverage') {
|
|
352
|
+
if (
|
|
353
|
+
!Array.isArray(brief.questions) ||
|
|
354
|
+
!Array.isArray(brief.claims) ||
|
|
355
|
+
!isObject(brief.scope)
|
|
356
|
+
) {
|
|
357
|
+
errors.push(
|
|
358
|
+
issue(
|
|
359
|
+
'INVALID_REVIEW_BRIEF',
|
|
360
|
+
'Coverage brief needs scope, questions, and claims',
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
for (const [index, claim] of (brief.claims ?? []).entries()) {
|
|
365
|
+
if (Object.keys(claim).sort().join(',') !== 'id,statement') {
|
|
366
|
+
errors.push(
|
|
367
|
+
issue(
|
|
368
|
+
'BLINDNESS_VIOLATION',
|
|
369
|
+
'Coverage claims have an invalid projection',
|
|
370
|
+
`$.claims[${index}]`,
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
errors.push(...validateArtifactShape(brief).errors);
|
|
377
|
+
return { valid: errors.length === 0, errors };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function writeReviewBrief({ packetRoot, outputPath, brief }) {
|
|
381
|
+
const root = resolve(packetRoot);
|
|
382
|
+
const target = resolve(outputPath);
|
|
383
|
+
await assertSafeOutputPath(root, target);
|
|
384
|
+
const result = validateReviewBrief(brief);
|
|
385
|
+
if (!result.valid) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Review brief violates selective blindness: ${result.errors[0].message}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
await mkdir(dirname(target), { recursive: true });
|
|
391
|
+
try {
|
|
392
|
+
await writeFile(target, `${canonicalJson(brief)}\n`, {
|
|
393
|
+
encoding: 'utf8',
|
|
394
|
+
flag: 'wx',
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error && typeof error === 'object' && error.code === 'EEXIST') {
|
|
398
|
+
throw new Error(`Review brief already exists: ${target}`, {
|
|
399
|
+
cause: error,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
return { path: relative(root, target), digest: await hashFile(target) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function main(argv) {
|
|
408
|
+
const options = Object.fromEntries(
|
|
409
|
+
argv.reduce((pairs, value, index) => {
|
|
410
|
+
if (value.startsWith('--')) pairs.push([value.slice(2), argv[index + 1]]);
|
|
411
|
+
return pairs;
|
|
412
|
+
}, []),
|
|
413
|
+
);
|
|
414
|
+
for (const key of [
|
|
415
|
+
'mode',
|
|
416
|
+
'id',
|
|
417
|
+
'created-at',
|
|
418
|
+
'manifest',
|
|
419
|
+
'ledger',
|
|
420
|
+
'output',
|
|
421
|
+
'packet-root',
|
|
422
|
+
]) {
|
|
423
|
+
if (!options[key]) throw new Error(`Missing --${key}`);
|
|
424
|
+
}
|
|
425
|
+
const [manifest, ledger] = await Promise.all([
|
|
426
|
+
readFile(options.manifest, 'utf8').then(JSON.parse),
|
|
427
|
+
readFile(options.ledger, 'utf8').then(JSON.parse),
|
|
428
|
+
]);
|
|
429
|
+
const brief = createReviewBrief({
|
|
430
|
+
mode: options.mode,
|
|
431
|
+
id: options.id,
|
|
432
|
+
createdAt: options['created-at'],
|
|
433
|
+
manifest,
|
|
434
|
+
ledger,
|
|
435
|
+
});
|
|
436
|
+
const reference = await writeReviewBrief({
|
|
437
|
+
packetRoot: options['packet-root'],
|
|
438
|
+
outputPath: options.output,
|
|
439
|
+
brief,
|
|
440
|
+
});
|
|
441
|
+
process.stdout.write(`${JSON.stringify(reference, null, 2)}\n`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (
|
|
445
|
+
process.argv[1] &&
|
|
446
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
447
|
+
) {
|
|
448
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
449
|
+
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
function canonicalize(value, seen) {
|
|
5
|
+
if (
|
|
6
|
+
value === null ||
|
|
7
|
+
typeof value === 'boolean' ||
|
|
8
|
+
typeof value === 'string'
|
|
9
|
+
) {
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === 'number') {
|
|
13
|
+
if (!Number.isFinite(value)) {
|
|
14
|
+
throw new TypeError('Canonical JSON cannot encode non-finite numbers');
|
|
15
|
+
}
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return `[${value.map((item) => canonicalize(item, seen)).join(',')}]`;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== 'object' || value === undefined) {
|
|
22
|
+
throw new TypeError(`Canonical JSON cannot encode ${typeof value}`);
|
|
23
|
+
}
|
|
24
|
+
if (seen.has(value)) {
|
|
25
|
+
throw new TypeError('Canonical JSON cannot encode cyclic values');
|
|
26
|
+
}
|
|
27
|
+
seen.add(value);
|
|
28
|
+
const entries = Object.keys(value)
|
|
29
|
+
.sort()
|
|
30
|
+
.map((key) => {
|
|
31
|
+
const item = value[key];
|
|
32
|
+
if (item === undefined) {
|
|
33
|
+
throw new TypeError(`Canonical JSON cannot encode undefined at ${key}`);
|
|
34
|
+
}
|
|
35
|
+
return `${JSON.stringify(key)}:${canonicalize(item, seen)}`;
|
|
36
|
+
});
|
|
37
|
+
seen.delete(value);
|
|
38
|
+
return `{${entries.join(',')}}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function canonicalJson(value) {
|
|
42
|
+
return canonicalize(value, new Set());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sha256(value) {
|
|
46
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hashCanonicalJson(value) {
|
|
50
|
+
return sha256(Buffer.from(canonicalJson(value), 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function hashFile(path) {
|
|
54
|
+
return sha256(await readFile(path));
|
|
55
|
+
}
|