@open-agent-toolkit/cli 0.2.53 → 0.2.55

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.
Files changed (81) hide show
  1. package/assets/agents/recon-worker.md +105 -0
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/cli-utilities/tool-packs.md +62 -8
  4. package/assets/docs/contributing/skills.md +26 -0
  5. package/assets/docs/workflows/skills/index.md +3 -0
  6. package/assets/docs/workflows/skills/recon.md +210 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
  9. package/assets/skills/recon/SKILL.md +298 -0
  10. package/assets/skills/recon/references/packet-contract.md +340 -0
  11. package/assets/skills/recon/references/profiles.md +58 -0
  12. package/assets/skills/recon/references/worker-contract.md +71 -0
  13. package/assets/skills/recon/scripts/create-review-brief.mjs +452 -0
  14. package/assets/skills/recon/scripts/lib/canonical-json.mjs +55 -0
  15. package/assets/skills/recon/scripts/lib/contracts.mjs +1969 -0
  16. package/assets/skills/recon/scripts/lib/safe-path.mjs +120 -0
  17. package/assets/skills/recon/scripts/lib/validated-run.mjs +80 -0
  18. package/assets/skills/recon/scripts/reconcile-ledger.mjs +360 -0
  19. package/assets/skills/recon/scripts/render-packet.mjs +379 -0
  20. package/assets/skills/recon/scripts/validate-artifact.mjs +96 -0
  21. package/assets/skills/recon/scripts/validate-packet.mjs +2192 -0
  22. package/dist/commands/doctor/index.js +5 -5
  23. package/dist/commands/init/index.d.ts.map +1 -1
  24. package/dist/commands/init/index.js +6 -7
  25. package/dist/commands/status/index.d.ts +2 -2
  26. package/dist/commands/status/index.d.ts.map +1 -1
  27. package/dist/commands/status/index.js +7 -4
  28. package/dist/commands/sync/index.d.ts.map +1 -1
  29. package/dist/commands/sync/index.js +24 -8
  30. package/dist/commands/sync/sync.types.d.ts +2 -2
  31. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  32. package/dist/commands/tools/migrate/index.d.ts.map +1 -1
  33. package/dist/commands/tools/migrate/index.js +25 -1
  34. package/dist/commands/tools/migrate/migrate-pack.d.ts +5 -0
  35. package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -1
  36. package/dist/commands/tools/migrate/migrate-pack.js +66 -7
  37. package/dist/commands/tools/remove/index.js +1 -1
  38. package/dist/commands/tools/remove/remove-tools.d.ts +5 -1
  39. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  40. package/dist/commands/tools/remove/remove-tools.js +81 -1
  41. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +3 -0
  42. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -1
  43. package/dist/commands/tools/shared/apply-pack-reconcile.js +40 -9
  44. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
  45. package/dist/commands/tools/shared/format-pack-inventory.js +3 -0
  46. package/dist/commands/tools/shared/install-sync-context.d.ts +2 -1
  47. package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
  48. package/dist/commands/tools/shared/install-sync-context.js +14 -3
  49. package/dist/commands/tools/shared/pack-dependencies.d.ts +14 -0
  50. package/dist/commands/tools/shared/pack-dependencies.d.ts.map +1 -0
  51. package/dist/commands/tools/shared/pack-dependencies.js +54 -0
  52. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  53. package/dist/commands/tools/shared/pack-inventory.js +9 -9
  54. package/dist/commands/tools/shared/pack-lifecycle.d.ts +9 -1
  55. package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -1
  56. package/dist/commands/tools/shared/pack-lifecycle.js +102 -9
  57. package/dist/commands/tools/shared/pack-manifest.d.ts +2 -2
  58. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  59. package/dist/commands/tools/shared/pack-manifest.js +91 -2
  60. package/dist/commands/tools/shared/pack-reconcile.d.ts +15 -1
  61. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
  62. package/dist/commands/tools/shared/pack-reconcile.js +75 -13
  63. package/dist/commands/tools/shared/scoped-pack-intent.d.ts +8 -0
  64. package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -1
  65. package/dist/commands/tools/shared/scoped-pack-intent.js +61 -15
  66. package/dist/commands/tools/shared/types.d.ts +7 -0
  67. package/dist/commands/tools/shared/types.d.ts.map +1 -1
  68. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  69. package/dist/commands/tools/update/update-tools.js +7 -1
  70. package/dist/config/oat-config.d.ts +5 -1
  71. package/dist/config/oat-config.d.ts.map +1 -1
  72. package/dist/config/oat-config.js +19 -0
  73. package/dist/engine/index.d.ts +1 -1
  74. package/dist/engine/index.d.ts.map +1 -1
  75. package/dist/engine/scanner.d.ts +9 -1
  76. package/dist/engine/scanner.d.ts.map +1 -1
  77. package/dist/engine/scanner.js +61 -4
  78. package/dist/shared/types.d.ts +1 -0
  79. package/dist/shared/types.d.ts.map +1 -1
  80. package/dist/shared/types.js +3 -0
  81. package/package.json +2 -2
@@ -0,0 +1,58 @@
1
+ # Recon Profiles
2
+
3
+ Profiles define assurance topology, not a model tier. The controller sizes
4
+ adaptive lanes from a mechanical source inventory, shows exact counts before
5
+ approval, and never exceeds the approved hard cap. Every wave remains
6
+ homogeneous and every pass uses the same approved model and effort.
7
+
8
+ ## quick
9
+
10
+ Use for bounded orientation where independently checked locators and supported
11
+ claims are sufficient.
12
+
13
+ - Required: one mapping wave, one or more non-overlapping gather lanes, one
14
+ compile lane, deterministic schema and locator validation, and rendering.
15
+ - Adaptive lane range: 1–4 gather lanes; hard cap 4.
16
+ - Maximum concurrency: 4.
17
+ - Assurance ceiling: `supported`; a quick packet is never `verified`.
18
+ - Conditional work: no semantic, adversarial, or coverage worker is implied.
19
+
20
+ ## standard
21
+
22
+ Use for load-bearing evidence that needs independent semantic challenge.
23
+
24
+ - Required: quick topology plus selectively blind semantic verification,
25
+ adversarial review, coverage review, and reconciliation.
26
+ - Adaptive lane range: 1–6 gather lanes and 1–3 verification lanes; hard cap 10
27
+ total worker lanes.
28
+ - Maximum concurrency: 6.
29
+ - Assurance ceiling: `verified` only after exact locator validation, affirmative
30
+ independent semantic review, no unresolved material challenge, and adequate
31
+ coverage.
32
+ - Conditional work: at most one approved reconciliation lane for a material
33
+ contradiction.
34
+
35
+ ## thorough
36
+
37
+ Use when expensive failure or correlated blind spots justify redundant source
38
+ reopening and explicit contradiction resolution.
39
+
40
+ - Required: standard topology plus redundant independent gathering and
41
+ verification for load-bearing claims and explicit contradiction resolution.
42
+ - Adaptive lane range: 2–10 gather lanes and 2–6 verification or adversarial
43
+ lanes; hard cap 20 total worker lanes.
44
+ - Maximum concurrency: 8.
45
+ - Assurance ceiling: `verified` only for claims affirmed by the required
46
+ independent passes and left without unresolved material challenge.
47
+ - Conditional work: up to two approved contradiction-resolution lanes within
48
+ the manifest's hard cap.
49
+
50
+ ## Planning Rules
51
+
52
+ 1. Partition by source or question so lane inputs and outputs never overlap.
53
+ 2. Classify required and conditional waves before target preparation.
54
+ 3. Compute the run-wide maximum model-class floor from every allowed wave.
55
+ 4. Reduce lanes when scope is small; never invent work to fill a profile cap.
56
+ 5. If runtime limits prevent the requested topology, prepare a different
57
+ profile for approval or publish an honest partial after an accepted failure.
58
+ 6. Never add a lane, retry, or replacement beyond the approved envelope.
@@ -0,0 +1,71 @@
1
+ # Recon Worker Contract
2
+
3
+ Every recon assignment is a non-interactive leaf task. The controller supplies
4
+ the complete assignment; a worker does not infer broader authority.
5
+
6
+ ## Required Assignment Envelope
7
+
8
+ The assignment must declare:
9
+
10
+ - `runId`, `waveId`, `laneId`, and exactly one mode;
11
+ - bounded objective, included scope, and excluded scope;
12
+ - allowed inputs and excluded inputs;
13
+ - source-read authority in `readSources`, including allowed read-only tools;
14
+ - sole `writePath`, contained by the packet directory and unique to the lane;
15
+ - required artifact `kind`, `schemaVersion`, and output schema;
16
+ - enforcement level and deadline; and
17
+ - whether failure should be recorded as required, optional, or conditional.
18
+
19
+ Reject an incomplete or contradictory assignment before reading sources. Never
20
+ request credentials, mutate an investigated source, broaden scope, or choose an
21
+ alternate write path.
22
+
23
+ ## Modes
24
+
25
+ - `map`: inventory only the assigned source partition and report candidate
26
+ evidence areas, unavailable inputs, and coverage gaps.
27
+ - `gather`: inspect assigned sources and emit source-grounded findings with
28
+ typed locators, minimal display excerpts, uncertainty, and contradictions.
29
+ - `compile`: consume only designated dossiers and create a provisional claim
30
+ ledger candidate. Deduplicate without inventing evidence.
31
+ - `verify`: consume an immutable verification brief, reopen only its declared
32
+ sources, test locators and claim semantics, and report claim dispositions.
33
+ - `adversary`: consume scope, questions, and provisional statements only; seek
34
+ counterevidence, unsupported inference, and missing alternatives.
35
+ - `coverage`: compare declared scope and questions with ledger coverage without
36
+ reading gatherer reasoning.
37
+ - `reconcile`: apply review dispositions and contradiction outcomes to a new
38
+ ledger candidate. Preserve prior revisions and never invent evidence.
39
+
40
+ No other mode is valid.
41
+
42
+ ## Output Contract
43
+
44
+ Write exactly one JSON artifact at `writePath`. The artifact must include:
45
+
46
+ - `kind`, `schemaVersion`, run/wave/lane identity, mode, and outcome;
47
+ - the exact allowed and excluded inputs actually honored;
48
+ - findings or dispositions with evidence IDs and typed locators;
49
+ - uncertainty, contradictions, and gaps as explicit arrays;
50
+ - safe categorical diagnostics for unavailable or invalid inputs; and
51
+ - direct input artifact references and digests when applicable.
52
+
53
+ Persist minimal excerpts only. Detect and redact secret spans before writing;
54
+ never persist the secret or its sensitive-span digest. Finish by returning the
55
+ artifact path and compact outcome only.
56
+
57
+ The controller validates the candidate against `packet-contract.md` with the
58
+ bundled deterministic artifact validator. A validation failure quarantines the
59
+ candidate; it never authorizes the worker to rewrite a shared artifact, retry,
60
+ or launch a replacement.
61
+
62
+ ## Invariants
63
+
64
+ - Read only the assignment's allowed inputs using its source-read authority.
65
+ - Write only the assigned artifact; never update a shared ledger or manifest.
66
+ - Never interact with the user.
67
+ - Never dispatch children, helpers, reviewers, or replacement workers.
68
+ - Never read excluded inputs, prior review conclusions, or raw reasoning that
69
+ the selected mode is required to remain blind to.
70
+ - Report uncertainty and contradiction instead of converting them to
71
+ confidence scores.
@@ -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
+ }