@open-agent-toolkit/cli 0.2.53 → 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.
Files changed (87) 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 +194 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/oat-dispatch-subagents/SKILL.md +105 -4
  9. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +226 -0
  10. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  11. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  12. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  13. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  14. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  15. package/assets/skills/recon/SKILL.md +260 -0
  16. package/assets/skills/recon/references/packet-contract.md +307 -0
  17. package/assets/skills/recon/references/profiles.md +58 -0
  18. package/assets/skills/recon/references/worker-contract.md +71 -0
  19. package/assets/skills/recon/scripts/create-review-brief.mjs +452 -0
  20. package/assets/skills/recon/scripts/lib/canonical-json.mjs +55 -0
  21. package/assets/skills/recon/scripts/lib/contracts.mjs +2462 -0
  22. package/assets/skills/recon/scripts/lib/safe-path.mjs +120 -0
  23. package/assets/skills/recon/scripts/lib/validated-run.mjs +86 -0
  24. package/assets/skills/recon/scripts/reconcile-ledger.mjs +360 -0
  25. package/assets/skills/recon/scripts/render-packet.mjs +392 -0
  26. package/assets/skills/recon/scripts/validate-artifact.mjs +96 -0
  27. package/assets/skills/recon/scripts/validate-packet.mjs +2359 -0
  28. package/dist/commands/doctor/index.js +5 -5
  29. package/dist/commands/init/index.d.ts.map +1 -1
  30. package/dist/commands/init/index.js +6 -7
  31. package/dist/commands/status/index.d.ts +2 -2
  32. package/dist/commands/status/index.d.ts.map +1 -1
  33. package/dist/commands/status/index.js +7 -4
  34. package/dist/commands/sync/index.d.ts.map +1 -1
  35. package/dist/commands/sync/index.js +24 -8
  36. package/dist/commands/sync/sync.types.d.ts +2 -2
  37. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  38. package/dist/commands/tools/migrate/index.d.ts.map +1 -1
  39. package/dist/commands/tools/migrate/index.js +25 -1
  40. package/dist/commands/tools/migrate/migrate-pack.d.ts +5 -0
  41. package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -1
  42. package/dist/commands/tools/migrate/migrate-pack.js +66 -7
  43. package/dist/commands/tools/remove/index.js +1 -1
  44. package/dist/commands/tools/remove/remove-tools.d.ts +5 -1
  45. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  46. package/dist/commands/tools/remove/remove-tools.js +81 -1
  47. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +3 -0
  48. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -1
  49. package/dist/commands/tools/shared/apply-pack-reconcile.js +40 -9
  50. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
  51. package/dist/commands/tools/shared/format-pack-inventory.js +3 -0
  52. package/dist/commands/tools/shared/install-sync-context.d.ts +2 -1
  53. package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
  54. package/dist/commands/tools/shared/install-sync-context.js +14 -3
  55. package/dist/commands/tools/shared/pack-dependencies.d.ts +14 -0
  56. package/dist/commands/tools/shared/pack-dependencies.d.ts.map +1 -0
  57. package/dist/commands/tools/shared/pack-dependencies.js +54 -0
  58. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  59. package/dist/commands/tools/shared/pack-inventory.js +9 -9
  60. package/dist/commands/tools/shared/pack-lifecycle.d.ts +9 -1
  61. package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -1
  62. package/dist/commands/tools/shared/pack-lifecycle.js +102 -9
  63. package/dist/commands/tools/shared/pack-manifest.d.ts +2 -2
  64. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  65. package/dist/commands/tools/shared/pack-manifest.js +91 -2
  66. package/dist/commands/tools/shared/pack-reconcile.d.ts +15 -1
  67. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
  68. package/dist/commands/tools/shared/pack-reconcile.js +75 -13
  69. package/dist/commands/tools/shared/scoped-pack-intent.d.ts +8 -0
  70. package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -1
  71. package/dist/commands/tools/shared/scoped-pack-intent.js +61 -15
  72. package/dist/commands/tools/shared/types.d.ts +7 -0
  73. package/dist/commands/tools/shared/types.d.ts.map +1 -1
  74. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  75. package/dist/commands/tools/update/update-tools.js +7 -1
  76. package/dist/config/oat-config.d.ts +5 -1
  77. package/dist/config/oat-config.d.ts.map +1 -1
  78. package/dist/config/oat-config.js +19 -0
  79. package/dist/engine/index.d.ts +1 -1
  80. package/dist/engine/index.d.ts.map +1 -1
  81. package/dist/engine/scanner.d.ts +9 -1
  82. package/dist/engine/scanner.d.ts.map +1 -1
  83. package/dist/engine/scanner.js +61 -4
  84. package/dist/shared/types.d.ts +1 -0
  85. package/dist/shared/types.d.ts.map +1 -1
  86. package/dist/shared/types.js +3 -0
  87. package/package.json +2 -2
@@ -0,0 +1,2462 @@
1
+ export const SCHEMA_VERSION = 1;
2
+
3
+ export const artifactKinds = new Set([
4
+ 'recon.packet-manifest',
5
+ 'recon.claim-ledger',
6
+ 'recon.raw-dossier',
7
+ 'recon.review-brief',
8
+ 'recon.review-result',
9
+ 'recon.stage-result',
10
+ 'recon.dispatch-receipt',
11
+ ]);
12
+
13
+ export const profiles = ['quick', 'standard', 'thorough'];
14
+ export const claimStates = [
15
+ 'provisional',
16
+ 'supported',
17
+ 'verified',
18
+ 'contested',
19
+ 'unresolved',
20
+ 'unsupported',
21
+ ];
22
+ export const locatorStates = ['exact', 'redacted-exact', 'stale', 'invalid'];
23
+ export const sourceValidationStates = [
24
+ 'pinned',
25
+ 'unpinned',
26
+ 'stale',
27
+ 'invalid',
28
+ 'unavailable',
29
+ ];
30
+ export const workerModes = [
31
+ 'map',
32
+ 'gather',
33
+ 'compile',
34
+ 'verify',
35
+ 'adversary',
36
+ 'coverage',
37
+ 'reconcile',
38
+ ];
39
+ export const stageModes = [
40
+ 'map',
41
+ 'gather',
42
+ 'compile',
43
+ 'locator-validation',
44
+ 'semantic-verification',
45
+ 'adversarial',
46
+ 'coverage',
47
+ 'reconciliation',
48
+ 'redundant-gather',
49
+ 'redundant-verification',
50
+ 'contradiction-resolution',
51
+ ];
52
+ export const taskClasses = [
53
+ 'mechanical-recon',
54
+ 'intelligent-recon',
55
+ 'default-implementation',
56
+ 'hard-reasoning',
57
+ 'consequential',
58
+ ];
59
+
60
+ const legalTransitions = new Set([
61
+ 'provisional:supported',
62
+ 'provisional:verified',
63
+ 'provisional:contested',
64
+ 'provisional:unresolved',
65
+ 'provisional:unsupported',
66
+ 'supported:verified',
67
+ 'supported:contested',
68
+ 'supported:unresolved',
69
+ 'supported:unsupported',
70
+ 'verified:contested',
71
+ 'verified:unresolved',
72
+ 'verified:unsupported',
73
+ 'contested:supported',
74
+ 'contested:verified',
75
+ 'contested:unresolved',
76
+ 'contested:unsupported',
77
+ 'unresolved:supported',
78
+ 'unresolved:verified',
79
+ 'unresolved:contested',
80
+ 'unresolved:unsupported',
81
+ 'unsupported:provisional',
82
+ ]);
83
+
84
+ export function issue(code, message, path = '$', severity = 'error') {
85
+ return { code, message, path, severity };
86
+ }
87
+
88
+ export function isObject(value) {
89
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
90
+ }
91
+
92
+ export function isDigest(value) {
93
+ return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
94
+ }
95
+
96
+ export function validateArtifactReference(reference, path = '$') {
97
+ const errors = [];
98
+ if (!isObject(reference)) {
99
+ return [issue('INVALID_ARTIFACT_REFERENCE', 'Expected an object', path)];
100
+ }
101
+ if (typeof reference.path !== 'string' || reference.path.length === 0) {
102
+ errors.push(
103
+ issue(
104
+ 'INVALID_ARTIFACT_REFERENCE',
105
+ 'Artifact path must be a non-empty string',
106
+ `${path}.path`,
107
+ ),
108
+ );
109
+ }
110
+ if (!isDigest(reference.digest)) {
111
+ errors.push(
112
+ issue(
113
+ 'INVALID_ARTIFACT_REFERENCE',
114
+ 'Artifact digest must be sha256:<64 lowercase hex characters>',
115
+ `${path}.digest`,
116
+ ),
117
+ );
118
+ }
119
+ return errors;
120
+ }
121
+
122
+ function validateExactReference(reference, path, errors, extra = []) {
123
+ errors.push(...validateArtifactReference(reference, path));
124
+ closedObject(reference, new Set(['path', 'digest', ...extra]), errors, path);
125
+ }
126
+
127
+ function requiredString(value, key, errors, path = '$') {
128
+ if (typeof value?.[key] !== 'string' || value[key].length === 0) {
129
+ errors.push(
130
+ issue(
131
+ 'MISSING_REQUIRED_FIELD',
132
+ `${key} must be a non-empty string`,
133
+ `${path}.${key}`,
134
+ ),
135
+ );
136
+ }
137
+ }
138
+
139
+ function requiredTimestamp(value, key, errors, path = '$') {
140
+ requiredString(value, key, errors, path);
141
+ if (
142
+ typeof value?.[key] === 'string' &&
143
+ !Number.isFinite(Date.parse(value[key]))
144
+ ) {
145
+ errors.push(
146
+ issue(
147
+ 'INVALID_TIMESTAMP',
148
+ `${key} must be a parseable timestamp`,
149
+ `${path}.${key}`,
150
+ ),
151
+ );
152
+ }
153
+ }
154
+
155
+ function requiredInteger(value, key, errors, path = '$', minimum = 0) {
156
+ if (!Number.isInteger(value?.[key]) || value[key] < minimum) {
157
+ errors.push(
158
+ issue(
159
+ 'MISSING_REQUIRED_FIELD',
160
+ `${key} must be an integer greater than or equal to ${minimum}`,
161
+ `${path}.${key}`,
162
+ ),
163
+ );
164
+ }
165
+ }
166
+
167
+ function requiredArray(value, key, errors, path = '$') {
168
+ if (!Array.isArray(value?.[key])) {
169
+ errors.push(
170
+ issue(
171
+ 'MISSING_REQUIRED_FIELD',
172
+ `${key} must be an array`,
173
+ `${path}.${key}`,
174
+ ),
175
+ );
176
+ }
177
+ }
178
+
179
+ function requiredCanonicalStringSet(value, key, errors, path = '$') {
180
+ requiredArray(value, key, errors, path);
181
+ if (!Array.isArray(value?.[key])) return;
182
+ const entries = value[key];
183
+ for (const [index, entry] of entries.entries()) {
184
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
185
+ errors.push(
186
+ issue(
187
+ 'INVALID_ARRAY_ENTRY',
188
+ `${key} entries must be non-empty strings`,
189
+ `${path}.${key}[${index}]`,
190
+ ),
191
+ );
192
+ }
193
+ }
194
+ if (new Set(entries).size !== entries.length) {
195
+ errors.push(
196
+ issue(
197
+ 'DUPLICATE_ARRAY_ENTRY',
198
+ `${key} entries must be unique`,
199
+ `${path}.${key}`,
200
+ ),
201
+ );
202
+ }
203
+ const sorted = entries.every((entry) => typeof entry === 'string')
204
+ ? [...entries].sort()
205
+ : null;
206
+ if (sorted && entries.some((entry, index) => entry !== sorted[index])) {
207
+ errors.push(
208
+ issue(
209
+ 'NON_CANONICAL_ARRAY_ORDER',
210
+ `${key} entries must use stable sorted order`,
211
+ `${path}.${key}`,
212
+ ),
213
+ );
214
+ }
215
+ }
216
+
217
+ function requiredObject(value, key, errors, path = '$') {
218
+ if (!isObject(value?.[key])) {
219
+ errors.push(
220
+ issue(
221
+ 'MISSING_REQUIRED_FIELD',
222
+ `${key} must be an object`,
223
+ `${path}.${key}`,
224
+ ),
225
+ );
226
+ }
227
+ }
228
+
229
+ function closedObject(value, allowed, errors, path = '$') {
230
+ if (!isObject(value)) return;
231
+ for (const key of Object.keys(value)) {
232
+ if (!allowed.has(key)) {
233
+ errors.push(
234
+ issue('UNKNOWN_FIELD', `Unexpected field ${key}`, `${path}.${key}`),
235
+ );
236
+ }
237
+ }
238
+ }
239
+
240
+ function duplicateIds(values, path, errors) {
241
+ const seen = new Set();
242
+ for (const [index, value] of values.entries()) {
243
+ if (!isObject(value) || typeof value.id !== 'string') continue;
244
+ if (seen.has(value.id)) {
245
+ errors.push(
246
+ issue(
247
+ 'DUPLICATE_ID',
248
+ `Duplicate identifier ${value.id}`,
249
+ `${path}[${index}].id`,
250
+ ),
251
+ );
252
+ }
253
+ seen.add(value.id);
254
+ }
255
+ }
256
+
257
+ const projectionKeys = new Set([
258
+ 'schema',
259
+ 'prepared_record_version',
260
+ 'run_id',
261
+ 'prepared_at',
262
+ 'request',
263
+ 'selection',
264
+ 'execution',
265
+ 'catalog_observation',
266
+ ]);
267
+ const requestKeys = new Set([
268
+ 'request_id',
269
+ 'caller',
270
+ 'objective',
271
+ 'action',
272
+ 'expected_output',
273
+ 'verification_evidence',
274
+ 'escalate_when',
275
+ ]);
276
+ const selectionKeys = new Set([
277
+ 'provider',
278
+ 'dispatch_context',
279
+ 'dispatch_policy',
280
+ 'dispatch_ceiling',
281
+ 'selected_route',
282
+ 'selection_source',
283
+ 'candidates_considered',
284
+ 'selection_reason',
285
+ 'role_name',
286
+ 'role_class',
287
+ 'role_selector',
288
+ 'model_selector',
289
+ 'model_selector_granularity',
290
+ 'effort_selector',
291
+ 'reasoning_mode_selector',
292
+ 'service_tier_selector',
293
+ 'guidance_reference',
294
+ 'guidance_version',
295
+ 'guidance_verified_at',
296
+ 'guidance_status',
297
+ ]);
298
+ const pinnedTargetKeys = new Set([
299
+ 'provider',
300
+ 'dispatch_context',
301
+ 'selected_route',
302
+ 'role_selector',
303
+ 'model_selector',
304
+ 'model_selector_granularity',
305
+ 'effort_selector',
306
+ 'reasoning_mode_selector',
307
+ 'service_tier_selector',
308
+ ]);
309
+ const waveKeys = new Set([
310
+ 'wave_id',
311
+ 'conditional',
312
+ 'task_class',
313
+ 'model_class_floor',
314
+ 'scope',
315
+ 'lanes',
316
+ 'authority',
317
+ 'authorization_scope',
318
+ 'writable_roots',
319
+ 'deadline_seconds',
320
+ 'retry_limit',
321
+ 'fallback',
322
+ 'dispatch_mode',
323
+ 'context_fork_controls',
324
+ 'concurrency',
325
+ 'lane_cap',
326
+ 'payload_digest',
327
+ ]);
328
+ const catalogKeys = new Set([
329
+ 'id',
330
+ 'source',
331
+ 'dispatch_context',
332
+ 'observed_at',
333
+ 'relevant_catalog_fingerprint',
334
+ ]);
335
+
336
+ export function validateApprovalProjection(value, errors, path) {
337
+ if (!isObject(value)) return;
338
+ closedObject(value, projectionKeys, errors, path);
339
+ if (value.schema !== 'oat-dispatch-approval/v1') {
340
+ errors.push(
341
+ issue(
342
+ 'INVALID_APPROVAL_SCHEMA',
343
+ 'Approval projection must use oat-dispatch-approval/v1',
344
+ `${path}.schema`,
345
+ ),
346
+ );
347
+ }
348
+ if (value.prepared_record_version !== 1) {
349
+ errors.push(
350
+ issue(
351
+ 'INVALID_PREPARED_RECORD_VERSION',
352
+ 'Prepared record version must be 1',
353
+ `${path}.prepared_record_version`,
354
+ ),
355
+ );
356
+ }
357
+ requiredString(value, 'run_id', errors, path);
358
+ requiredString(value, 'prepared_at', errors, path);
359
+ requiredObject(value, 'request', errors, path);
360
+ requiredObject(value, 'selection', errors, path);
361
+ requiredObject(value, 'execution', errors, path);
362
+ requiredObject(value, 'catalog_observation', errors, path);
363
+
364
+ closedObject(value.request, requestKeys, errors, `${path}.request`);
365
+ for (const key of [
366
+ 'request_id',
367
+ 'caller',
368
+ 'objective',
369
+ 'action',
370
+ 'expected_output',
371
+ 'verification_evidence',
372
+ ]) {
373
+ requiredString(value.request, key, errors, `${path}.request`);
374
+ }
375
+ requiredCanonicalStringSet(
376
+ value.request,
377
+ 'escalate_when',
378
+ errors,
379
+ `${path}.request`,
380
+ );
381
+
382
+ closedObject(value.selection, selectionKeys, errors, `${path}.selection`);
383
+ for (const key of [...selectionKeys].filter(
384
+ (item) => item !== 'candidates_considered',
385
+ )) {
386
+ if (
387
+ key === 'reasoning_mode_selector' &&
388
+ value.selection?.reasoning_mode_selector === null
389
+ ) {
390
+ continue;
391
+ }
392
+ requiredString(value.selection, key, errors, `${path}.selection`);
393
+ }
394
+ requiredCanonicalStringSet(
395
+ value.selection,
396
+ 'candidates_considered',
397
+ errors,
398
+ `${path}.selection`,
399
+ );
400
+
401
+ closedObject(
402
+ value.execution,
403
+ new Set(['waves', 'run_maximum_floor', 'pinned_target']),
404
+ errors,
405
+ `${path}.execution`,
406
+ );
407
+ requiredArray(value.execution, 'waves', errors, `${path}.execution`);
408
+ requiredString(
409
+ value.execution,
410
+ 'run_maximum_floor',
411
+ errors,
412
+ `${path}.execution`,
413
+ );
414
+ requiredObject(value.execution, 'pinned_target', errors, `${path}.execution`);
415
+ const waveIds = new Set();
416
+ const laneIds = new Set();
417
+ for (const [waveIndex, wave] of (value.execution?.waves ?? []).entries()) {
418
+ const wavePath = `${path}.execution.waves[${waveIndex}]`;
419
+ closedObject(wave, waveKeys, errors, wavePath);
420
+ for (const key of [
421
+ 'wave_id',
422
+ 'task_class',
423
+ 'model_class_floor',
424
+ 'scope',
425
+ 'authority',
426
+ 'authorization_scope',
427
+ 'dispatch_mode',
428
+ ]) {
429
+ requiredString(wave, key, errors, wavePath);
430
+ }
431
+ requiredArray(wave, 'lanes', errors, wavePath);
432
+ requiredCanonicalStringSet(wave, 'writable_roots', errors, wavePath);
433
+ requiredObject(wave, 'fallback', errors, wavePath);
434
+ requiredObject(wave, 'context_fork_controls', errors, wavePath);
435
+ requiredInteger(wave, 'deadline_seconds', errors, wavePath, 1);
436
+ requiredInteger(wave, 'retry_limit', errors, wavePath);
437
+ requiredInteger(wave, 'concurrency', errors, wavePath, 1);
438
+ requiredInteger(wave, 'lane_cap', errors, wavePath, 1);
439
+ if (typeof wave.conditional !== 'boolean') {
440
+ errors.push(
441
+ issue(
442
+ 'MISSING_REQUIRED_FIELD',
443
+ 'conditional must be a boolean',
444
+ `${wavePath}.conditional`,
445
+ ),
446
+ );
447
+ }
448
+ if (!taskClasses.includes(wave.task_class)) {
449
+ errors.push(
450
+ issue(
451
+ 'INVALID_TASK_CLASS',
452
+ 'Unknown wave task class',
453
+ `${wavePath}.task_class`,
454
+ ),
455
+ );
456
+ }
457
+ if (wave.model_class_floor !== wave.task_class) {
458
+ errors.push(
459
+ issue(
460
+ 'MODEL_CLASS_FLOOR_MISMATCH',
461
+ 'Wave model class floor must equal its task class',
462
+ `${wavePath}.model_class_floor`,
463
+ ),
464
+ );
465
+ }
466
+ if (!isDigest(wave.payload_digest)) {
467
+ errors.push(
468
+ issue(
469
+ 'INVALID_PAYLOAD_DIGEST',
470
+ 'Wave payload digest must be sha256',
471
+ `${wavePath}.payload_digest`,
472
+ ),
473
+ );
474
+ }
475
+ if (waveIds.has(wave.wave_id)) {
476
+ errors.push(
477
+ issue(
478
+ 'DUPLICATE_ID',
479
+ `Duplicate wave ${wave.wave_id}`,
480
+ `${wavePath}.wave_id`,
481
+ ),
482
+ );
483
+ }
484
+ waveIds.add(wave.wave_id);
485
+ closedObject(
486
+ wave.fallback,
487
+ new Set(['mode']),
488
+ errors,
489
+ `${wavePath}.fallback`,
490
+ );
491
+ requiredString(wave.fallback, 'mode', errors, `${wavePath}.fallback`);
492
+ closedObject(
493
+ wave.context_fork_controls,
494
+ new Set(['fork_turns']),
495
+ errors,
496
+ `${wavePath}.context_fork_controls`,
497
+ );
498
+ requiredString(
499
+ wave.context_fork_controls,
500
+ 'fork_turns',
501
+ errors,
502
+ `${wavePath}.context_fork_controls`,
503
+ );
504
+ for (const [laneIndex, lane] of (wave.lanes ?? []).entries()) {
505
+ const lanePath = `${wavePath}.lanes[${laneIndex}]`;
506
+ closedObject(lane, new Set(['lane_id', 'scope']), errors, lanePath);
507
+ requiredString(lane, 'lane_id', errors, lanePath);
508
+ requiredString(lane, 'scope', errors, lanePath);
509
+ if (laneIds.has(lane.lane_id)) {
510
+ errors.push(
511
+ issue(
512
+ 'DUPLICATE_ID',
513
+ `Duplicate lane ${lane.lane_id}`,
514
+ `${lanePath}.lane_id`,
515
+ ),
516
+ );
517
+ }
518
+ laneIds.add(lane.lane_id);
519
+ }
520
+ }
521
+
522
+ const floors = (value.execution?.waves ?? [])
523
+ .map((wave) => taskClasses.indexOf(wave.model_class_floor))
524
+ .filter((index) => index >= 0);
525
+ const maximumFloor = taskClasses[Math.max(...floors)];
526
+ if (maximumFloor && value.execution?.run_maximum_floor !== maximumFloor) {
527
+ errors.push(
528
+ issue(
529
+ 'RUN_MAXIMUM_FLOOR_MISMATCH',
530
+ 'Run maximum floor does not equal the strongest wave floor',
531
+ `${path}.execution.run_maximum_floor`,
532
+ ),
533
+ );
534
+ }
535
+ if (!taskClasses.includes(value.execution?.run_maximum_floor)) {
536
+ errors.push(
537
+ issue(
538
+ 'INVALID_TASK_CLASS',
539
+ 'Unknown run maximum floor',
540
+ `${path}.execution.run_maximum_floor`,
541
+ ),
542
+ );
543
+ }
544
+ closedObject(
545
+ value.execution?.pinned_target,
546
+ pinnedTargetKeys,
547
+ errors,
548
+ `${path}.execution.pinned_target`,
549
+ );
550
+ for (const key of pinnedTargetKeys) {
551
+ if (
552
+ key === 'reasoning_mode_selector' &&
553
+ value.execution?.pinned_target?.reasoning_mode_selector === null
554
+ ) {
555
+ continue;
556
+ }
557
+ requiredString(
558
+ value.execution?.pinned_target,
559
+ key,
560
+ errors,
561
+ `${path}.execution.pinned_target`,
562
+ );
563
+ }
564
+ for (const key of pinnedTargetKeys) {
565
+ if (value.execution?.pinned_target?.[key] !== value.selection?.[key]) {
566
+ errors.push(
567
+ issue(
568
+ 'PINNED_TARGET_MISMATCH',
569
+ `${key} differs from the approved selection`,
570
+ `${path}.execution.pinned_target.${key}`,
571
+ ),
572
+ );
573
+ }
574
+ }
575
+
576
+ closedObject(
577
+ value.catalog_observation,
578
+ catalogKeys,
579
+ errors,
580
+ `${path}.catalog_observation`,
581
+ );
582
+ for (const key of ['id', 'source', 'dispatch_context', 'observed_at']) {
583
+ requiredString(
584
+ value.catalog_observation,
585
+ key,
586
+ errors,
587
+ `${path}.catalog_observation`,
588
+ );
589
+ }
590
+ if (!isDigest(value.catalog_observation?.relevant_catalog_fingerprint)) {
591
+ errors.push(
592
+ issue(
593
+ 'INVALID_CATALOG_FINGERPRINT',
594
+ 'Catalog fingerprint must be sha256',
595
+ `${path}.catalog_observation.relevant_catalog_fingerprint`,
596
+ ),
597
+ );
598
+ }
599
+ if (
600
+ value.catalog_observation?.dispatch_context !==
601
+ value.selection?.dispatch_context
602
+ ) {
603
+ errors.push(
604
+ issue(
605
+ 'CATALOG_CONTEXT_MISMATCH',
606
+ 'Catalog context differs from the approved selection',
607
+ `${path}.catalog_observation.dispatch_context`,
608
+ ),
609
+ );
610
+ }
611
+ }
612
+
613
+ function validateApprovalEvidence(value, errors, path) {
614
+ if (!isObject(value)) return;
615
+ closedObject(value, new Set(['type', 'fingerprint']), errors, path);
616
+ if (value.type !== 'explicit-user-approval') {
617
+ errors.push(
618
+ issue(
619
+ 'INVALID_APPROVAL_EVIDENCE',
620
+ 'Approval evidence must record explicit user approval',
621
+ `${path}.type`,
622
+ ),
623
+ );
624
+ }
625
+ if (!isDigest(value.fingerprint)) {
626
+ errors.push(
627
+ issue(
628
+ 'INVALID_APPROVAL_FINGERPRINT',
629
+ 'Approval evidence fingerprint must be sha256',
630
+ `${path}.fingerprint`,
631
+ ),
632
+ );
633
+ }
634
+ }
635
+
636
+ function validateCatalogRecheck(value, errors, path) {
637
+ if (!isObject(value)) return;
638
+ closedObject(value, catalogKeys, errors, path);
639
+ for (const key of ['id', 'source', 'dispatch_context']) {
640
+ requiredString(value, key, errors, path);
641
+ }
642
+ requiredTimestamp(value, 'observed_at', errors, path);
643
+ if (!isDigest(value.relevant_catalog_fingerprint)) {
644
+ errors.push(
645
+ issue(
646
+ 'INVALID_CATALOG_FINGERPRINT',
647
+ 'Catalog recheck fingerprint must be sha256',
648
+ `${path}.relevant_catalog_fingerprint`,
649
+ ),
650
+ );
651
+ }
652
+ }
653
+
654
+ function validateManifest(value, errors) {
655
+ closedObject(
656
+ value,
657
+ new Set([
658
+ 'kind',
659
+ 'schemaVersion',
660
+ 'run',
661
+ 'request',
662
+ 'sources',
663
+ 'execution',
664
+ 'stages',
665
+ 'artifacts',
666
+ 'gaps',
667
+ ]),
668
+ errors,
669
+ );
670
+ if (!isObject(value.run)) {
671
+ errors.push(
672
+ issue('MISSING_REQUIRED_FIELD', 'run must be an object', '$.run'),
673
+ );
674
+ } else {
675
+ closedObject(
676
+ value.run,
677
+ new Set([
678
+ 'id',
679
+ 'topic',
680
+ 'status',
681
+ 'requestedProfile',
682
+ 'achievedProfile',
683
+ 'createdAt',
684
+ 'updatedAt',
685
+ ]),
686
+ errors,
687
+ '$.run',
688
+ );
689
+ requiredString(value.run, 'id', errors, '$.run');
690
+ requiredString(value.run, 'topic', errors, '$.run');
691
+ if (
692
+ ![
693
+ 'preparing',
694
+ 'awaiting-approval',
695
+ 'running',
696
+ 'complete',
697
+ 'partial',
698
+ 'failed',
699
+ ].includes(value.run.status)
700
+ ) {
701
+ errors.push(
702
+ issue('INVALID_RUN_STATUS', 'Unknown run status', '$.run.status'),
703
+ );
704
+ }
705
+ if (!profiles.includes(value.run.requestedProfile)) {
706
+ errors.push(
707
+ issue(
708
+ 'INVALID_PROFILE',
709
+ 'Unknown requested profile',
710
+ '$.run.requestedProfile',
711
+ ),
712
+ );
713
+ }
714
+ if (
715
+ value.run.achievedProfile !== null &&
716
+ !profiles.includes(value.run.achievedProfile)
717
+ ) {
718
+ errors.push(
719
+ issue(
720
+ 'INVALID_PROFILE',
721
+ 'Unknown achieved profile',
722
+ '$.run.achievedProfile',
723
+ ),
724
+ );
725
+ }
726
+ }
727
+ requiredArray(value, 'sources', errors);
728
+ requiredArray(value, 'stages', errors);
729
+ requiredArray(value, 'artifacts', errors);
730
+ requiredArray(value, 'gaps', errors);
731
+ requiredObject(value, 'request', errors);
732
+ requiredObject(value, 'execution', errors);
733
+ if (isObject(value.request)) {
734
+ closedObject(
735
+ value.request,
736
+ new Set([
737
+ 'objective',
738
+ 'questions',
739
+ 'includedScope',
740
+ 'excludedScope',
741
+ 'contextReferences',
742
+ 'outputPath',
743
+ ]),
744
+ errors,
745
+ '$.request',
746
+ );
747
+ requiredString(value.request, 'objective', errors, '$.request');
748
+ requiredString(value.request, 'outputPath', errors, '$.request');
749
+ for (const key of [
750
+ 'questions',
751
+ 'includedScope',
752
+ 'excludedScope',
753
+ 'contextReferences',
754
+ ]) {
755
+ requiredArray(value.request, key, errors, '$.request');
756
+ }
757
+ }
758
+ if (isObject(value.execution)) {
759
+ closedObject(
760
+ value.execution,
761
+ new Set([
762
+ 'approvalProjection',
763
+ 'approvalCanonicalJson',
764
+ 'approvalFingerprint',
765
+ 'approvedAt',
766
+ 'approvalEvidence',
767
+ 'catalogRecheck',
768
+ ]),
769
+ errors,
770
+ '$.execution',
771
+ );
772
+ requiredObject(
773
+ value.execution,
774
+ 'approvalProjection',
775
+ errors,
776
+ '$.execution',
777
+ );
778
+ requiredString(
779
+ value.execution,
780
+ 'approvalCanonicalJson',
781
+ errors,
782
+ '$.execution',
783
+ );
784
+ requiredString(
785
+ value.execution,
786
+ 'approvalFingerprint',
787
+ errors,
788
+ '$.execution',
789
+ );
790
+ requiredTimestamp(value.execution, 'approvedAt', errors, '$.execution');
791
+ requiredObject(value.execution, 'approvalEvidence', errors, '$.execution');
792
+ requiredObject(value.execution, 'catalogRecheck', errors, '$.execution');
793
+ validateApprovalProjection(
794
+ value.execution.approvalProjection,
795
+ errors,
796
+ '$.execution.approvalProjection',
797
+ );
798
+ validateApprovalEvidence(
799
+ value.execution.approvalEvidence,
800
+ errors,
801
+ '$.execution.approvalEvidence',
802
+ );
803
+ validateCatalogRecheck(
804
+ value.execution.catalogRecheck,
805
+ errors,
806
+ '$.execution.catalogRecheck',
807
+ );
808
+ }
809
+ if (Array.isArray(value.sources))
810
+ duplicateIds(value.sources, '$.sources', errors);
811
+ if (Array.isArray(value.stages))
812
+ duplicateIds(value.stages, '$.stages', errors);
813
+ if (Array.isArray(value.gaps)) duplicateIds(value.gaps, '$.gaps', errors);
814
+ for (const [index, reference] of (value.artifacts ?? []).entries()) {
815
+ validateExactReference(reference, `$.artifacts[${index}]`, errors);
816
+ }
817
+ for (const [index, source] of (value.sources ?? []).entries()) {
818
+ for (const key of [
819
+ 'kind',
820
+ 'id',
821
+ 'authority',
822
+ 'observedAt',
823
+ 'validationState',
824
+ ]) {
825
+ requiredString(source, key, errors, `$.sources[${index}]`);
826
+ }
827
+ if (typeof source.available !== 'boolean') {
828
+ errors.push(
829
+ issue(
830
+ 'MISSING_REQUIRED_FIELD',
831
+ 'available must be a boolean',
832
+ `$.sources[${index}].available`,
833
+ ),
834
+ );
835
+ }
836
+ if (!sourceValidationStates.includes(source.validationState)) {
837
+ errors.push(
838
+ issue(
839
+ 'INVALID_SOURCE_VALIDATION_STATE',
840
+ 'Unknown source validation state',
841
+ `$.sources[${index}].validationState`,
842
+ ),
843
+ );
844
+ }
845
+ if (
846
+ (source.available === false) !==
847
+ (source.validationState === 'unavailable')
848
+ ) {
849
+ errors.push(
850
+ issue(
851
+ 'SOURCE_AVAILABILITY_MISMATCH',
852
+ 'Unavailable sources must use unavailable validation state and vice versa',
853
+ `$.sources[${index}]`,
854
+ ),
855
+ );
856
+ }
857
+ const common = [
858
+ 'kind',
859
+ 'id',
860
+ 'available',
861
+ 'authority',
862
+ 'observedAt',
863
+ 'validationState',
864
+ ];
865
+ const kindKeys = {
866
+ repository: ['root', 'revision', 'dirty', 'contentHashes'],
867
+ file: ['path', 'contentHash'],
868
+ url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
869
+ 'command-output': [
870
+ 'argv',
871
+ 'cwd',
872
+ 'exitStatus',
873
+ 'outputPath',
874
+ 'outputDigest',
875
+ 'environmentNames',
876
+ ],
877
+ 'connected-resource': [
878
+ 'system',
879
+ 'resourceId',
880
+ 'resourceVersion',
881
+ 'retrievalToken',
882
+ 'capturePath',
883
+ 'captureDigest',
884
+ ],
885
+ }[source.kind];
886
+ if (!kindKeys) {
887
+ errors.push(
888
+ issue(
889
+ 'INVALID_SOURCE_KIND',
890
+ 'Unknown source kind',
891
+ `$.sources[${index}].kind`,
892
+ ),
893
+ );
894
+ } else {
895
+ closedObject(
896
+ source,
897
+ new Set([...common, ...kindKeys]),
898
+ errors,
899
+ `$.sources[${index}]`,
900
+ );
901
+ if (source.kind === 'url' && isObject(source.validatorState)) {
902
+ closedObject(
903
+ source.validatorState,
904
+ new Set(['etag', 'lastModified', 'capturePath', 'captureDigest']),
905
+ errors,
906
+ `$.sources[${index}].validatorState`,
907
+ );
908
+ const hasDirectCapture =
909
+ Object.hasOwn(source, 'capturePath') ||
910
+ Object.hasOwn(source, 'captureDigest');
911
+ const hasValidatorCapture =
912
+ Object.hasOwn(source.validatorState, 'capturePath') ||
913
+ Object.hasOwn(source.validatorState, 'captureDigest');
914
+ if (hasDirectCapture && hasValidatorCapture) {
915
+ errors.push(
916
+ issue(
917
+ 'DUAL_URL_CAPTURE',
918
+ 'URL sources must declare exactly one capture representation',
919
+ `$.sources[${index}]`,
920
+ ),
921
+ );
922
+ }
923
+ }
924
+ }
925
+ }
926
+ for (const [index, gap] of (value.gaps ?? []).entries()) {
927
+ requiredString(gap, 'id', errors, `$.gaps[${index}]`);
928
+ requiredString(gap, 'code', errors, `$.gaps[${index}]`);
929
+ requiredString(gap, 'message', errors, `$.gaps[${index}]`);
930
+ if (typeof gap.material !== 'boolean') {
931
+ errors.push(
932
+ issue(
933
+ 'MISSING_REQUIRED_FIELD',
934
+ 'material must be a boolean',
935
+ `$.gaps[${index}].material`,
936
+ ),
937
+ );
938
+ }
939
+ closedObject(
940
+ gap,
941
+ new Set([
942
+ 'id',
943
+ 'code',
944
+ 'message',
945
+ 'material',
946
+ 'sourceIds',
947
+ 'claimIds',
948
+ 'coverageFindingIds',
949
+ ]),
950
+ errors,
951
+ `$.gaps[${index}]`,
952
+ );
953
+ }
954
+ for (const [index, stage] of (value.stages ?? []).entries()) {
955
+ requiredString(stage, 'id', errors, `$.stages[${index}]`);
956
+ requiredString(stage, 'waveId', errors, `$.stages[${index}]`);
957
+ requiredString(stage, 'mode', errors, `$.stages[${index}]`);
958
+ requiredString(stage, 'status', errors, `$.stages[${index}]`);
959
+ requiredArray(stage, 'artifactIds', errors, `$.stages[${index}]`);
960
+ requiredArray(stage, 'dispatchReceiptIds', errors, `$.stages[${index}]`);
961
+ requiredString(stage, 'laneId', errors, `$.stages[${index}]`);
962
+ if (!stageModes.includes(stage.mode)) {
963
+ errors.push(
964
+ issue(
965
+ 'INVALID_STAGE_MODE',
966
+ 'Unknown stage mode',
967
+ `$.stages[${index}].mode`,
968
+ ),
969
+ );
970
+ }
971
+ if (!['complete', 'failed', 'omitted'].includes(stage.status)) {
972
+ errors.push(
973
+ issue(
974
+ 'INVALID_STAGE_STATUS',
975
+ 'Unknown stage status',
976
+ `$.stages[${index}].status`,
977
+ ),
978
+ );
979
+ }
980
+ if (
981
+ stage.kind !== 'recon.stage-result' ||
982
+ stage.schemaVersion !== SCHEMA_VERSION
983
+ ) {
984
+ errors.push(
985
+ issue(
986
+ 'INVALID_STAGE_RESULT',
987
+ 'Expected recon.stage-result version 1',
988
+ `$.stages[${index}]`,
989
+ ),
990
+ );
991
+ }
992
+ closedObject(
993
+ stage,
994
+ new Set([
995
+ 'kind',
996
+ 'schemaVersion',
997
+ 'id',
998
+ 'waveId',
999
+ 'mode',
1000
+ 'status',
1001
+ 'artifactIds',
1002
+ 'dispatchReceiptIds',
1003
+ 'laneId',
1004
+ 'message',
1005
+ ]),
1006
+ errors,
1007
+ `$.stages[${index}]`,
1008
+ );
1009
+ }
1010
+ }
1011
+
1012
+ function validateLedger(value, errors) {
1013
+ closedObject(
1014
+ value,
1015
+ new Set([
1016
+ 'kind',
1017
+ 'schemaVersion',
1018
+ 'runId',
1019
+ 'revision',
1020
+ 'inputArtifacts',
1021
+ 'synthesis',
1022
+ 'evidence',
1023
+ 'claims',
1024
+ 'unresolvedQuestions',
1025
+ 'transitions',
1026
+ ]),
1027
+ errors,
1028
+ );
1029
+ requiredString(value, 'runId', errors);
1030
+ if (!Number.isInteger(value.revision) || value.revision < 1) {
1031
+ errors.push(
1032
+ issue(
1033
+ 'INVALID_REVISION',
1034
+ 'revision must be a positive integer',
1035
+ '$.revision',
1036
+ ),
1037
+ );
1038
+ }
1039
+ for (const key of [
1040
+ 'inputArtifacts',
1041
+ 'evidence',
1042
+ 'claims',
1043
+ 'unresolvedQuestions',
1044
+ ]) {
1045
+ requiredArray(value, key, errors);
1046
+ }
1047
+ requiredArray(value, 'transitions', errors);
1048
+ requiredObject(value, 'synthesis', errors);
1049
+ if (isObject(value.synthesis)) {
1050
+ requiredString(value.synthesis, 'answer', errors, '$.synthesis');
1051
+ for (const key of ['keyClaimIds', 'caveats', 'unresolvedQuestionIds']) {
1052
+ requiredArray(value.synthesis, key, errors, '$.synthesis');
1053
+ }
1054
+ closedObject(
1055
+ value.synthesis,
1056
+ new Set(['answer', 'keyClaimIds', 'caveats', 'unresolvedQuestionIds']),
1057
+ errors,
1058
+ '$.synthesis',
1059
+ );
1060
+ const claimIds = new Set(
1061
+ Array.isArray(value.claims)
1062
+ ? value.claims.map((claim) => claim?.id).filter(Boolean)
1063
+ : [],
1064
+ );
1065
+ if (Array.isArray(value.synthesis.keyClaimIds)) {
1066
+ for (const [index, claimId] of value.synthesis.keyClaimIds.entries()) {
1067
+ if (typeof claimId !== 'string' || claimId.trim().length === 0) {
1068
+ errors.push(
1069
+ issue(
1070
+ 'INVALID_ARRAY_ENTRY',
1071
+ 'keyClaimIds entries must be non-empty strings',
1072
+ `$.synthesis.keyClaimIds[${index}]`,
1073
+ ),
1074
+ );
1075
+ } else if (!claimIds.has(claimId)) {
1076
+ errors.push(
1077
+ issue(
1078
+ 'SYNTHESIS_REFERENCE_MISSING',
1079
+ `Synthesis key claim ${claimId} does not resolve to an existing claim`,
1080
+ `$.synthesis.keyClaimIds[${index}]`,
1081
+ ),
1082
+ );
1083
+ }
1084
+ }
1085
+ }
1086
+ const questionIds = new Set(
1087
+ Array.isArray(value.unresolvedQuestions)
1088
+ ? value.unresolvedQuestions.map((q) => q?.id).filter(Boolean)
1089
+ : [],
1090
+ );
1091
+ if (Array.isArray(value.synthesis.unresolvedQuestionIds)) {
1092
+ for (const [
1093
+ index,
1094
+ questionId,
1095
+ ] of value.synthesis.unresolvedQuestionIds.entries()) {
1096
+ if (typeof questionId !== 'string' || questionId.trim().length === 0) {
1097
+ errors.push(
1098
+ issue(
1099
+ 'INVALID_ARRAY_ENTRY',
1100
+ 'unresolvedQuestionIds entries must be non-empty strings',
1101
+ `$.synthesis.unresolvedQuestionIds[${index}]`,
1102
+ ),
1103
+ );
1104
+ } else if (!questionIds.has(questionId)) {
1105
+ errors.push(
1106
+ issue(
1107
+ 'SYNTHESIS_REFERENCE_MISSING',
1108
+ `Synthesis unresolved question ${questionId} does not resolve to an existing question`,
1109
+ `$.synthesis.unresolvedQuestionIds[${index}]`,
1110
+ ),
1111
+ );
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ for (const key of ['evidence', 'claims', 'unresolvedQuestions']) {
1117
+ if (Array.isArray(value[key])) duplicateIds(value[key], `$.${key}`, errors);
1118
+ }
1119
+ for (const [index, reference] of (value.inputArtifacts ?? []).entries()) {
1120
+ validateExactReference(reference, `$.inputArtifacts[${index}]`, errors);
1121
+ }
1122
+ for (const [index, evidence] of (value.evidence ?? []).entries()) {
1123
+ closedObject(
1124
+ evidence,
1125
+ new Set([
1126
+ 'id',
1127
+ 'sourceId',
1128
+ 'locator',
1129
+ 'displayExcerpt',
1130
+ 'observedAt',
1131
+ 'contentHash',
1132
+ 'locatorValidation',
1133
+ 'provenance',
1134
+ 'redaction',
1135
+ ]),
1136
+ errors,
1137
+ `$.evidence[${index}]`,
1138
+ );
1139
+ requiredString(evidence, 'id', errors, `$.evidence[${index}]`);
1140
+ requiredString(evidence, 'sourceId', errors, `$.evidence[${index}]`);
1141
+ requiredString(evidence, 'displayExcerpt', errors, `$.evidence[${index}]`);
1142
+ requiredString(evidence, 'observedAt', errors, `$.evidence[${index}]`);
1143
+ if (!isObject(evidence.locator)) {
1144
+ errors.push(
1145
+ issue(
1146
+ 'INVALID_LOCATOR',
1147
+ 'locator must be an object',
1148
+ `$.evidence[${index}].locator`,
1149
+ ),
1150
+ );
1151
+ } else {
1152
+ const locatorKeys = {
1153
+ repository: ['kind', 'path', 'revision', 'lineStart', 'lineEnd'],
1154
+ file: ['kind', 'path', 'lineStart', 'lineEnd'],
1155
+ url: ['kind', 'url', 'retrievedAt', 'fragment', 'validatorToken'],
1156
+ 'command-output': [
1157
+ 'kind',
1158
+ 'artifactPath',
1159
+ 'lineStart',
1160
+ 'lineEnd',
1161
+ 'commandDigest',
1162
+ ],
1163
+ 'connected-resource': [
1164
+ 'kind',
1165
+ 'system',
1166
+ 'resourceId',
1167
+ 'resourceVersion',
1168
+ 'retrievalToken',
1169
+ 'fieldOrSection',
1170
+ 'retrievedAt',
1171
+ ],
1172
+ }[evidence.locator.kind];
1173
+ if (!locatorKeys) {
1174
+ errors.push(
1175
+ issue(
1176
+ 'INVALID_LOCATOR',
1177
+ 'Unknown locator kind',
1178
+ `$.evidence[${index}].locator.kind`,
1179
+ ),
1180
+ );
1181
+ } else {
1182
+ closedObject(
1183
+ evidence.locator,
1184
+ new Set(locatorKeys),
1185
+ errors,
1186
+ `$.evidence[${index}].locator`,
1187
+ );
1188
+ }
1189
+ }
1190
+ if (!locatorStates.includes(evidence.locatorValidation?.status)) {
1191
+ errors.push(
1192
+ issue(
1193
+ 'INVALID_LOCATOR_STATE',
1194
+ 'Unknown locator validation state',
1195
+ `$.evidence[${index}].locatorValidation.status`,
1196
+ ),
1197
+ );
1198
+ }
1199
+ if (isObject(evidence.locatorValidation)) {
1200
+ closedObject(
1201
+ evidence.locatorValidation,
1202
+ new Set(['status', 'validatedAt']),
1203
+ errors,
1204
+ `$.evidence[${index}].locatorValidation`,
1205
+ );
1206
+ requiredString(
1207
+ evidence.locatorValidation,
1208
+ 'validatedAt',
1209
+ errors,
1210
+ `$.evidence[${index}].locatorValidation`,
1211
+ );
1212
+ }
1213
+ validateExactReference(
1214
+ evidence.provenance,
1215
+ `$.evidence[${index}].provenance`,
1216
+ errors,
1217
+ );
1218
+ }
1219
+ for (const [index, claim] of (value.claims ?? []).entries()) {
1220
+ closedObject(
1221
+ claim,
1222
+ new Set([
1223
+ 'id',
1224
+ 'statement',
1225
+ 'status',
1226
+ 'evidence',
1227
+ 'qualifications',
1228
+ 'reviewIds',
1229
+ 'derivedFrom',
1230
+ 'challenges',
1231
+ ]),
1232
+ errors,
1233
+ `$.claims[${index}]`,
1234
+ );
1235
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
1236
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
1237
+ if (!claimStates.includes(claim.status)) {
1238
+ errors.push(
1239
+ issue(
1240
+ 'INVALID_CLAIM_STATE',
1241
+ 'Unknown claim state',
1242
+ `$.claims[${index}].status`,
1243
+ ),
1244
+ );
1245
+ }
1246
+ for (const key of [
1247
+ 'evidence',
1248
+ 'qualifications',
1249
+ 'reviewIds',
1250
+ 'derivedFrom',
1251
+ 'challenges',
1252
+ ]) {
1253
+ requiredArray(claim, key, errors, `$.claims[${index}]`);
1254
+ }
1255
+ for (const [referenceIndex, reference] of (
1256
+ claim.derivedFrom ?? []
1257
+ ).entries()) {
1258
+ validateExactReference(
1259
+ reference,
1260
+ `$.claims[${index}].derivedFrom[${referenceIndex}]`,
1261
+ errors,
1262
+ );
1263
+ }
1264
+ }
1265
+ for (const [index, transition] of (value.transitions ?? []).entries()) {
1266
+ closedObject(
1267
+ transition,
1268
+ new Set(['claimId', 'from', 'to']),
1269
+ errors,
1270
+ `$.transitions[${index}]`,
1271
+ );
1272
+ if (!legalTransitions.has(`${transition.from}:${transition.to}`)) {
1273
+ errors.push(
1274
+ issue(
1275
+ 'ILLEGAL_CLAIM_TRANSITION',
1276
+ `Illegal claim transition ${transition.from} -> ${transition.to}`,
1277
+ `$.transitions[${index}]`,
1278
+ ),
1279
+ );
1280
+ }
1281
+ }
1282
+ const claimIds = new Set((value.claims ?? []).map((claim) => claim.id));
1283
+ const lastTransitionByClaim = new Map();
1284
+ for (const transition of value.transitions ?? []) {
1285
+ if (!claimIds.has(transition.claimId)) {
1286
+ errors.push(
1287
+ issue(
1288
+ 'UNKNOWN_TRANSITION_CLAIM',
1289
+ `Transition references unknown claim ${transition.claimId}`,
1290
+ '$.transitions',
1291
+ ),
1292
+ );
1293
+ }
1294
+ lastTransitionByClaim.set(transition.claimId, transition);
1295
+ }
1296
+ for (const claim of value.claims ?? []) {
1297
+ const lastTransition = lastTransitionByClaim.get(claim.id);
1298
+ if (value.revision === 1 && claim.status === 'provisional') {
1299
+ if (lastTransition) {
1300
+ errors.push(
1301
+ issue(
1302
+ 'INVALID_PROVISIONAL_GENESIS',
1303
+ `Revision-one provisional claim ${claim.id} must use genesis without an incoming transition`,
1304
+ `claim:${claim.id}`,
1305
+ ),
1306
+ );
1307
+ }
1308
+ continue;
1309
+ }
1310
+ if (
1311
+ (value.revision === 1 || lastTransition) &&
1312
+ lastTransition?.to !== claim.status
1313
+ ) {
1314
+ errors.push(
1315
+ issue(
1316
+ 'CLAIM_TRANSITION_MISMATCH',
1317
+ `Final transition does not establish ${claim.id} as ${claim.status}`,
1318
+ `claim:${claim.id}`,
1319
+ ),
1320
+ );
1321
+ }
1322
+ if (
1323
+ value.revision > 1 &&
1324
+ claim.status === 'provisional' &&
1325
+ lastTransition?.to !== 'provisional'
1326
+ ) {
1327
+ errors.push(
1328
+ issue(
1329
+ 'INVALID_PROVISIONAL_GENESIS',
1330
+ `Later revision provisional claim ${claim.id} requires a legal incoming transition`,
1331
+ `claim:${claim.id}`,
1332
+ ),
1333
+ );
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ function validateDossier(value, errors) {
1339
+ closedObject(
1340
+ value,
1341
+ new Set([
1342
+ 'kind',
1343
+ 'schemaVersion',
1344
+ 'id',
1345
+ 'runId',
1346
+ 'waveId',
1347
+ 'laneId',
1348
+ 'mode',
1349
+ 'outcome',
1350
+ 'allowedInputs',
1351
+ 'excludedInputs',
1352
+ 'findings',
1353
+ 'uncertainty',
1354
+ 'contradictions',
1355
+ 'gaps',
1356
+ ]),
1357
+ errors,
1358
+ );
1359
+ for (const key of ['runId', 'waveId', 'laneId', 'mode', 'outcome']) {
1360
+ requiredString(value, key, errors);
1361
+ }
1362
+ if (!workerModes.includes(value.mode)) {
1363
+ errors.push(issue('INVALID_WORKER_MODE', 'Unknown worker mode', '$.mode'));
1364
+ }
1365
+ if (!['complete', 'partial', 'failed'].includes(value.outcome)) {
1366
+ errors.push(
1367
+ issue('INVALID_DOSSIER_OUTCOME', 'Unknown dossier outcome', '$.outcome'),
1368
+ );
1369
+ }
1370
+ for (const key of [
1371
+ 'allowedInputs',
1372
+ 'excludedInputs',
1373
+ 'findings',
1374
+ 'uncertainty',
1375
+ 'contradictions',
1376
+ 'gaps',
1377
+ ]) {
1378
+ requiredArray(value, key, errors);
1379
+ }
1380
+ }
1381
+
1382
+ const reviewSourceKindFields = {
1383
+ repository: ['root', 'revision', 'dirty', 'contentHashes'],
1384
+ file: ['path', 'contentHash'],
1385
+ url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
1386
+ 'command-output': [
1387
+ 'argv',
1388
+ 'cwd',
1389
+ 'exitStatus',
1390
+ 'outputPath',
1391
+ 'outputDigest',
1392
+ 'environmentNames',
1393
+ ],
1394
+ 'connected-resource': [
1395
+ 'system',
1396
+ 'resourceId',
1397
+ 'resourceVersion',
1398
+ 'retrievalToken',
1399
+ 'capturePath',
1400
+ 'captureDigest',
1401
+ ],
1402
+ };
1403
+ const commonReviewSourceFields = [
1404
+ 'id',
1405
+ 'kind',
1406
+ 'available',
1407
+ 'authority',
1408
+ 'observedAt',
1409
+ 'validationState',
1410
+ ];
1411
+
1412
+ function validateReviewBriefSource(source, index, errors) {
1413
+ const path = `$.sources[${index}]`;
1414
+ if (!isObject(source)) {
1415
+ errors.push(
1416
+ issue(
1417
+ 'INVALID_SOURCE_DESCRIPTOR',
1418
+ 'Verification brief sources must be objects',
1419
+ path,
1420
+ ),
1421
+ );
1422
+ return;
1423
+ }
1424
+ for (const key of commonReviewSourceFields) {
1425
+ if (key === 'available') continue;
1426
+ requiredString(source, key, errors, path);
1427
+ }
1428
+ if (typeof source.available !== 'boolean') {
1429
+ errors.push(
1430
+ issue(
1431
+ 'MISSING_REQUIRED_FIELD',
1432
+ 'available must be a boolean',
1433
+ `${path}.available`,
1434
+ ),
1435
+ );
1436
+ }
1437
+ if (!sourceValidationStates.includes(source.validationState)) {
1438
+ errors.push(
1439
+ issue(
1440
+ 'INVALID_SOURCE_VALIDATION_STATE',
1441
+ 'Unknown source validation state',
1442
+ `${path}.validationState`,
1443
+ ),
1444
+ );
1445
+ }
1446
+ const kindFields = reviewSourceKindFields[source.kind];
1447
+ if (!kindFields) {
1448
+ errors.push(
1449
+ issue('INVALID_SOURCE_KIND', 'Unknown source kind', `${path}.kind`),
1450
+ );
1451
+ return;
1452
+ }
1453
+ closedObject(
1454
+ source,
1455
+ new Set([...commonReviewSourceFields, ...kindFields]),
1456
+ errors,
1457
+ path,
1458
+ );
1459
+
1460
+ if (source.kind === 'repository') {
1461
+ requiredString(source, 'root', errors, path);
1462
+ requiredString(source, 'revision', errors, path);
1463
+ requiredObject(source, 'contentHashes', errors, path);
1464
+ if (typeof source.dirty !== 'boolean') {
1465
+ errors.push(
1466
+ issue(
1467
+ 'MISSING_REQUIRED_FIELD',
1468
+ 'dirty must be a boolean',
1469
+ `${path}.dirty`,
1470
+ ),
1471
+ );
1472
+ }
1473
+ } else if (source.kind === 'file') {
1474
+ requiredString(source, 'path', errors, path);
1475
+ requiredString(source, 'contentHash', errors, path);
1476
+ if (!isDigest(source.contentHash)) {
1477
+ errors.push(
1478
+ issue(
1479
+ 'INVALID_SOURCE_DESCRIPTOR',
1480
+ 'contentHash must be a sha256 digest',
1481
+ `${path}.contentHash`,
1482
+ ),
1483
+ );
1484
+ }
1485
+ } else if (source.kind === 'url') {
1486
+ requiredString(source, 'url', errors, path);
1487
+ const hasDirectCaptureFields =
1488
+ Object.hasOwn(source, 'capturePath') ||
1489
+ Object.hasOwn(source, 'captureDigest');
1490
+ if (hasDirectCaptureFields) {
1491
+ requiredString(source, 'capturePath', errors, path);
1492
+ requiredString(source, 'captureDigest', errors, path);
1493
+ if (!isDigest(source.captureDigest)) {
1494
+ errors.push(
1495
+ issue(
1496
+ 'INVALID_SOURCE_DESCRIPTOR',
1497
+ 'captureDigest must be a sha256 digest',
1498
+ `${path}.captureDigest`,
1499
+ ),
1500
+ );
1501
+ }
1502
+ }
1503
+ const hasDirectCapture =
1504
+ typeof source.capturePath === 'string' &&
1505
+ source.capturePath.trim().length > 0 &&
1506
+ isDigest(source.captureDigest);
1507
+ let hasValidValidatorState = false;
1508
+ let hasValidatorCapture = false;
1509
+ if (Object.hasOwn(source, 'validatorState')) {
1510
+ requiredObject(source, 'validatorState', errors, path);
1511
+ if (isObject(source.validatorState)) {
1512
+ const validatorPath = `${path}.validatorState`;
1513
+ closedObject(
1514
+ source.validatorState,
1515
+ new Set(['etag', 'lastModified', 'capturePath', 'captureDigest']),
1516
+ errors,
1517
+ validatorPath,
1518
+ );
1519
+ const validatorKeys = Object.keys(source.validatorState);
1520
+ const hasValidatorCaptureFields =
1521
+ Object.hasOwn(source.validatorState, 'capturePath') ||
1522
+ Object.hasOwn(source.validatorState, 'captureDigest');
1523
+ if (Object.hasOwn(source.validatorState, 'etag')) {
1524
+ requiredString(source.validatorState, 'etag', errors, validatorPath);
1525
+ }
1526
+ if (Object.hasOwn(source.validatorState, 'lastModified')) {
1527
+ requiredString(
1528
+ source.validatorState,
1529
+ 'lastModified',
1530
+ errors,
1531
+ validatorPath,
1532
+ );
1533
+ }
1534
+ if (hasValidatorCaptureFields) {
1535
+ requiredString(
1536
+ source.validatorState,
1537
+ 'capturePath',
1538
+ errors,
1539
+ validatorPath,
1540
+ );
1541
+ requiredString(
1542
+ source.validatorState,
1543
+ 'captureDigest',
1544
+ errors,
1545
+ validatorPath,
1546
+ );
1547
+ hasValidatorCapture =
1548
+ typeof source.validatorState.capturePath === 'string' &&
1549
+ source.validatorState.capturePath.trim().length > 0 &&
1550
+ isDigest(source.validatorState.captureDigest);
1551
+ if (!hasValidatorCapture) {
1552
+ errors.push(
1553
+ issue(
1554
+ 'INVALID_SOURCE_DESCRIPTOR',
1555
+ 'validatorState capturePath and captureDigest must form a complete sha256-bound pair',
1556
+ validatorPath,
1557
+ ),
1558
+ );
1559
+ }
1560
+ }
1561
+ hasValidValidatorState =
1562
+ (typeof source.validatorState.etag === 'string' &&
1563
+ source.validatorState.etag.trim().length > 0) ||
1564
+ (typeof source.validatorState.lastModified === 'string' &&
1565
+ source.validatorState.lastModified.trim().length > 0) ||
1566
+ hasValidatorCapture;
1567
+ if (validatorKeys.length === 0 || !hasValidValidatorState) {
1568
+ errors.push(
1569
+ issue(
1570
+ 'INVALID_SOURCE_DESCRIPTOR',
1571
+ 'validatorState must contain an etag, lastModified value, or complete capture pair',
1572
+ validatorPath,
1573
+ ),
1574
+ );
1575
+ }
1576
+ }
1577
+ }
1578
+ if (hasDirectCapture && hasValidatorCapture) {
1579
+ errors.push(
1580
+ issue(
1581
+ 'DUAL_URL_CAPTURE',
1582
+ 'URL sources cannot declare direct and validatorState capture pairs',
1583
+ path,
1584
+ ),
1585
+ );
1586
+ }
1587
+ if (!hasDirectCapture && !hasValidValidatorState) {
1588
+ errors.push(
1589
+ issue(
1590
+ 'MISSING_REQUIRED_FIELD',
1591
+ 'URL sources require a capture or validator state',
1592
+ path,
1593
+ ),
1594
+ );
1595
+ }
1596
+ } else if (source.kind === 'command-output') {
1597
+ requiredArray(source, 'argv', errors, path);
1598
+ requiredString(source, 'cwd', errors, path);
1599
+ requiredInteger(source, 'exitStatus', errors, path);
1600
+ requiredString(source, 'outputPath', errors, path);
1601
+ requiredString(source, 'outputDigest', errors, path);
1602
+ requiredArray(source, 'environmentNames', errors, path);
1603
+ if (!isDigest(source.outputDigest)) {
1604
+ errors.push(
1605
+ issue(
1606
+ 'INVALID_SOURCE_DESCRIPTOR',
1607
+ 'outputDigest must be a sha256 digest',
1608
+ `${path}.outputDigest`,
1609
+ ),
1610
+ );
1611
+ }
1612
+ } else {
1613
+ requiredString(source, 'system', errors, path);
1614
+ requiredString(source, 'resourceId', errors, path);
1615
+ const hasVersion =
1616
+ (typeof source.resourceVersion === 'string' &&
1617
+ source.resourceVersion.trim().length > 0) ||
1618
+ (typeof source.retrievalToken === 'string' &&
1619
+ source.retrievalToken.trim().length > 0);
1620
+ const hasCapture =
1621
+ Object.hasOwn(source, 'capturePath') ||
1622
+ Object.hasOwn(source, 'captureDigest');
1623
+ if (hasCapture) {
1624
+ requiredString(source, 'capturePath', errors, path);
1625
+ requiredString(source, 'captureDigest', errors, path);
1626
+ if (!isDigest(source.captureDigest)) {
1627
+ errors.push(
1628
+ issue(
1629
+ 'INVALID_SOURCE_DESCRIPTOR',
1630
+ 'captureDigest must be a sha256 digest',
1631
+ `${path}.captureDigest`,
1632
+ ),
1633
+ );
1634
+ }
1635
+ }
1636
+ if (!hasVersion && !hasCapture) {
1637
+ errors.push(
1638
+ issue(
1639
+ 'MISSING_REQUIRED_FIELD',
1640
+ 'Connected sources require a version, retrieval token, or capture',
1641
+ path,
1642
+ ),
1643
+ );
1644
+ }
1645
+ }
1646
+ }
1647
+
1648
+ function validateReviewBriefArtifact(value, errors) {
1649
+ for (const key of ['id', 'runId', 'mode', 'createdAt']) {
1650
+ requiredString(value, key, errors);
1651
+ }
1652
+ requiredArray(value, 'excludedInputs', errors);
1653
+ if (value.mode === 'verify') {
1654
+ requiredArray(value, 'claims', errors);
1655
+ requiredArray(value, 'sources', errors);
1656
+ closedObject(
1657
+ value,
1658
+ new Set([
1659
+ 'kind',
1660
+ 'schemaVersion',
1661
+ 'id',
1662
+ 'runId',
1663
+ 'mode',
1664
+ 'createdAt',
1665
+ 'excludedInputs',
1666
+ 'claims',
1667
+ 'sources',
1668
+ ]),
1669
+ errors,
1670
+ );
1671
+ for (const [index, claim] of (value.claims ?? []).entries()) {
1672
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
1673
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
1674
+ requiredArray(claim, 'evidence', errors, `$.claims[${index}]`);
1675
+ closedObject(
1676
+ claim,
1677
+ new Set(['id', 'statement', 'evidence']),
1678
+ errors,
1679
+ `$.claims[${index}]`,
1680
+ );
1681
+ for (const [evidenceIndex, evidence] of (
1682
+ claim.evidence ?? []
1683
+ ).entries()) {
1684
+ for (const key of ['id', 'sourceId', 'displayExcerpt']) {
1685
+ requiredString(
1686
+ evidence,
1687
+ key,
1688
+ errors,
1689
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1690
+ );
1691
+ }
1692
+ requiredObject(
1693
+ evidence,
1694
+ 'locator',
1695
+ errors,
1696
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1697
+ );
1698
+ closedObject(
1699
+ evidence,
1700
+ new Set(['id', 'sourceId', 'displayExcerpt', 'locator']),
1701
+ errors,
1702
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1703
+ );
1704
+ }
1705
+ }
1706
+ for (const [index, source] of (value.sources ?? []).entries()) {
1707
+ validateReviewBriefSource(source, index, errors);
1708
+ }
1709
+ if (Array.isArray(value.sources)) {
1710
+ duplicateIds(value.sources, '$.sources', errors);
1711
+ const suppliedSourceIds = new Set(
1712
+ value.sources
1713
+ .map((source) => source?.id)
1714
+ .filter((id) => typeof id === 'string'),
1715
+ );
1716
+ const referencedSourceIds = new Set();
1717
+ for (const [claimIndex, claim] of (value.claims ?? []).entries()) {
1718
+ for (const [evidenceIndex, evidence] of (
1719
+ claim.evidence ?? []
1720
+ ).entries()) {
1721
+ if (typeof evidence?.sourceId !== 'string') continue;
1722
+ referencedSourceIds.add(evidence.sourceId);
1723
+ if (!suppliedSourceIds.has(evidence.sourceId)) {
1724
+ errors.push(
1725
+ issue(
1726
+ 'MISSING_SOURCE_DESCRIPTOR',
1727
+ `Evidence source ${evidence.sourceId} does not resolve to a verification brief source`,
1728
+ `$.claims[${claimIndex}].evidence[${evidenceIndex}].sourceId`,
1729
+ ),
1730
+ );
1731
+ }
1732
+ }
1733
+ }
1734
+ for (const [index, source] of value.sources.entries()) {
1735
+ if (
1736
+ typeof source?.id === 'string' &&
1737
+ !referencedSourceIds.has(source.id)
1738
+ ) {
1739
+ errors.push(
1740
+ issue(
1741
+ 'UNREFERENCED_SOURCE_DESCRIPTOR',
1742
+ `Verification brief source ${source.id} is not referenced by any claim evidence`,
1743
+ `$.sources[${index}].id`,
1744
+ ),
1745
+ );
1746
+ }
1747
+ }
1748
+ }
1749
+ } else if (value.mode === 'adversary') {
1750
+ requiredObject(value, 'scope', errors);
1751
+ requiredArray(value, 'questions', errors);
1752
+ requiredArray(value, 'provisionalStatements', errors);
1753
+ closedObject(
1754
+ value,
1755
+ new Set([
1756
+ 'kind',
1757
+ 'schemaVersion',
1758
+ 'id',
1759
+ 'runId',
1760
+ 'mode',
1761
+ 'createdAt',
1762
+ 'excludedInputs',
1763
+ 'scope',
1764
+ 'questions',
1765
+ 'provisionalStatements',
1766
+ ]),
1767
+ errors,
1768
+ );
1769
+ closedObject(
1770
+ value.scope,
1771
+ new Set(['included', 'excluded']),
1772
+ errors,
1773
+ '$.scope',
1774
+ );
1775
+ for (const key of ['included', 'excluded']) {
1776
+ requiredArray(value.scope, key, errors, '$.scope');
1777
+ }
1778
+ for (const [index, statement] of (
1779
+ value.provisionalStatements ?? []
1780
+ ).entries()) {
1781
+ requiredString(
1782
+ statement,
1783
+ 'id',
1784
+ errors,
1785
+ `$.provisionalStatements[${index}]`,
1786
+ );
1787
+ requiredString(
1788
+ statement,
1789
+ 'statement',
1790
+ errors,
1791
+ `$.provisionalStatements[${index}]`,
1792
+ );
1793
+ closedObject(
1794
+ statement,
1795
+ new Set(['id', 'statement']),
1796
+ errors,
1797
+ `$.provisionalStatements[${index}]`,
1798
+ );
1799
+ }
1800
+ } else if (value.mode === 'coverage') {
1801
+ requiredObject(value, 'scope', errors);
1802
+ requiredArray(value, 'questions', errors);
1803
+ requiredArray(value, 'claims', errors);
1804
+ closedObject(
1805
+ value,
1806
+ new Set([
1807
+ 'kind',
1808
+ 'schemaVersion',
1809
+ 'id',
1810
+ 'runId',
1811
+ 'mode',
1812
+ 'createdAt',
1813
+ 'excludedInputs',
1814
+ 'scope',
1815
+ 'questions',
1816
+ 'claims',
1817
+ ]),
1818
+ errors,
1819
+ );
1820
+ closedObject(
1821
+ value.scope,
1822
+ new Set(['included', 'excluded']),
1823
+ errors,
1824
+ '$.scope',
1825
+ );
1826
+ for (const key of ['included', 'excluded']) {
1827
+ requiredArray(value.scope, key, errors, '$.scope');
1828
+ }
1829
+ for (const [index, claim] of (value.claims ?? []).entries()) {
1830
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
1831
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
1832
+ closedObject(
1833
+ claim,
1834
+ new Set(['id', 'statement']),
1835
+ errors,
1836
+ `$.claims[${index}]`,
1837
+ );
1838
+ }
1839
+ } else {
1840
+ errors.push(
1841
+ issue('INVALID_REVIEW_MODE', 'Unknown review brief mode', '$.mode'),
1842
+ );
1843
+ }
1844
+ }
1845
+
1846
+ function validateReviewResult(value, errors) {
1847
+ for (const key of ['id', 'runId', 'reviewKind', 'reviewerLane', 'status']) {
1848
+ requiredString(value, key, errors);
1849
+ }
1850
+ for (const key of [
1851
+ 'permittedInputs',
1852
+ 'excludedInputs',
1853
+ 'dispositions',
1854
+ 'newEvidence',
1855
+ 'coverageFindings',
1856
+ 'unresolvedIssues',
1857
+ ]) {
1858
+ requiredArray(value, key, errors);
1859
+ }
1860
+ if (
1861
+ ![
1862
+ 'semantic',
1863
+ 'adversarial',
1864
+ 'coverage',
1865
+ 'redundant-verification',
1866
+ 'contradiction-resolution',
1867
+ 'reconciliation',
1868
+ ].includes(value.reviewKind)
1869
+ ) {
1870
+ errors.push(
1871
+ issue(
1872
+ 'INVALID_REVIEW_KIND',
1873
+ 'Unknown review result kind',
1874
+ '$.reviewKind',
1875
+ ),
1876
+ );
1877
+ }
1878
+ if (!['complete', 'failed'].includes(value.status)) {
1879
+ errors.push(
1880
+ issue('INVALID_REVIEW_STATUS', 'Unknown review status', '$.status'),
1881
+ );
1882
+ }
1883
+ if (value.reviewKind === 'reconciliation') {
1884
+ requiredObject(value, 'inputLedger', errors);
1885
+ if (isObject(value.inputLedger)) {
1886
+ validateExactReference(value.inputLedger, '$.inputLedger', errors, [
1887
+ 'revision',
1888
+ ]);
1889
+ if (!Number.isInteger(value.inputLedger.revision)) {
1890
+ errors.push(
1891
+ issue(
1892
+ 'INVALID_REVISION',
1893
+ 'input ledger revision must be an integer',
1894
+ '$.inputLedger.revision',
1895
+ ),
1896
+ );
1897
+ }
1898
+ }
1899
+ if (!Number.isInteger(value.outputRevision)) {
1900
+ errors.push(
1901
+ issue(
1902
+ 'INVALID_REVISION',
1903
+ 'outputRevision must be an integer',
1904
+ '$.outputRevision',
1905
+ ),
1906
+ );
1907
+ }
1908
+ requiredArray(value, 'incorporatedReviewIds', errors);
1909
+ requiredArray(value, 'transitions', errors);
1910
+ requiredArray(value, 'additions', errors);
1911
+ requiredArray(value, 'removals', errors);
1912
+ requiredArray(value, 'removalDispositions', errors);
1913
+ requiredArray(value, 'coverageDispositions', errors);
1914
+ for (const key of ['additions', 'removals']) {
1915
+ const seen = new Set();
1916
+ for (const [index, id] of (value[key] ?? []).entries()) {
1917
+ if (typeof id !== 'string' || id.length === 0 || seen.has(id)) {
1918
+ errors.push(
1919
+ issue(
1920
+ 'INVALID_RECONCILIATION_MEMBERSHIP',
1921
+ `${key} must contain unique non-empty claim IDs`,
1922
+ `$.${key}[${index}]`,
1923
+ ),
1924
+ );
1925
+ }
1926
+ seen.add(id);
1927
+ }
1928
+ }
1929
+ for (const [index, disposition] of (
1930
+ value.removalDispositions ?? []
1931
+ ).entries()) {
1932
+ for (const key of ['claimId', 'reviewId', 'disposition']) {
1933
+ requiredString(
1934
+ disposition,
1935
+ key,
1936
+ errors,
1937
+ `$.removalDispositions[${index}]`,
1938
+ );
1939
+ }
1940
+ if (disposition.disposition !== 'rejected') {
1941
+ errors.push(
1942
+ issue(
1943
+ 'INVALID_REMOVAL_DISPOSITION',
1944
+ 'Claim removal must be authorized by a rejected review disposition',
1945
+ `$.removalDispositions[${index}].disposition`,
1946
+ ),
1947
+ );
1948
+ }
1949
+ closedObject(
1950
+ disposition,
1951
+ new Set(['claimId', 'reviewId', 'disposition']),
1952
+ errors,
1953
+ `$.removalDispositions[${index}]`,
1954
+ );
1955
+ }
1956
+ for (const [index, transition] of (value.transitions ?? []).entries()) {
1957
+ closedObject(
1958
+ transition,
1959
+ new Set(['claimId', 'from', 'to']),
1960
+ errors,
1961
+ `$.transitions[${index}]`,
1962
+ );
1963
+ requiredString(transition, 'claimId', errors, `$.transitions[${index}]`);
1964
+ if (!legalTransitions.has(`${transition.from}:${transition.to}`)) {
1965
+ errors.push(
1966
+ issue(
1967
+ 'ILLEGAL_CLAIM_TRANSITION',
1968
+ `Illegal claim transition ${transition.from} -> ${transition.to}`,
1969
+ `$.transitions[${index}]`,
1970
+ ),
1971
+ );
1972
+ }
1973
+ }
1974
+ for (const [index, disposition] of (
1975
+ value.coverageDispositions ?? []
1976
+ ).entries()) {
1977
+ for (const key of ['findingId', 'gapId', 'disposition']) {
1978
+ requiredString(
1979
+ disposition,
1980
+ key,
1981
+ errors,
1982
+ `$.coverageDispositions[${index}]`,
1983
+ );
1984
+ }
1985
+ if (!['accepted-gap', 'resolved'].includes(disposition.disposition)) {
1986
+ errors.push(
1987
+ issue(
1988
+ 'INVALID_COVERAGE_DISPOSITION',
1989
+ 'Coverage dispositions must be accepted-gap or resolved',
1990
+ `$.coverageDispositions[${index}].disposition`,
1991
+ ),
1992
+ );
1993
+ }
1994
+ if (
1995
+ disposition.disposition === 'resolved' &&
1996
+ (!Array.isArray(disposition.evidenceIds) ||
1997
+ disposition.evidenceIds.length === 0)
1998
+ ) {
1999
+ errors.push(
2000
+ issue(
2001
+ 'INVALID_COVERAGE_DISPOSITION',
2002
+ 'Resolved coverage dispositions require typed evidence IDs',
2003
+ `$.coverageDispositions[${index}].evidenceIds`,
2004
+ ),
2005
+ );
2006
+ }
2007
+ closedObject(
2008
+ disposition,
2009
+ new Set(['findingId', 'gapId', 'disposition', 'evidenceIds']),
2010
+ errors,
2011
+ `$.coverageDispositions[${index}]`,
2012
+ );
2013
+ }
2014
+ } else {
2015
+ validateExactReference(value.brief, '$.brief', errors);
2016
+ }
2017
+ for (const [index, reference] of (value.permittedInputs ?? []).entries()) {
2018
+ validateExactReference(reference, `$.permittedInputs[${index}]`, errors);
2019
+ }
2020
+ for (const [index, disposition] of (value.dispositions ?? []).entries()) {
2021
+ requiredString(disposition, 'claimId', errors, `$.dispositions[${index}]`);
2022
+ requiredString(
2023
+ disposition,
2024
+ 'disposition',
2025
+ errors,
2026
+ `$.dispositions[${index}]`,
2027
+ );
2028
+ closedObject(
2029
+ disposition,
2030
+ new Set(['claimId', 'disposition']),
2031
+ errors,
2032
+ `$.dispositions[${index}]`,
2033
+ );
2034
+ const allowed = {
2035
+ semantic: ['affirmed', 'rejected', 'uncertain'],
2036
+ adversarial: ['unchallenged', 'challenged'],
2037
+ coverage: ['covered', 'gap'],
2038
+ 'redundant-verification': ['affirmed', 'rejected', 'uncertain'],
2039
+ 'contradiction-resolution': ['resolved', 'unresolved', 'rejected'],
2040
+ reconciliation: [],
2041
+ }[value.reviewKind];
2042
+ if (allowed && !allowed.includes(disposition.disposition)) {
2043
+ errors.push(
2044
+ issue(
2045
+ 'INVALID_REVIEW_DISPOSITION',
2046
+ 'Disposition does not belong to this review kind',
2047
+ `$.dispositions[${index}].disposition`,
2048
+ ),
2049
+ );
2050
+ }
2051
+ }
2052
+ if (value.reviewKind === 'contradiction-resolution') {
2053
+ requiredArray(value, 'contradictionDispositions', errors);
2054
+ for (const [index, disposition] of (
2055
+ value.contradictionDispositions ?? []
2056
+ ).entries()) {
2057
+ requiredString(
2058
+ disposition,
2059
+ 'contradictionId',
2060
+ errors,
2061
+ `$.contradictionDispositions[${index}]`,
2062
+ );
2063
+ requiredArray(
2064
+ disposition,
2065
+ 'claimIds',
2066
+ errors,
2067
+ `$.contradictionDispositions[${index}]`,
2068
+ );
2069
+ requiredString(
2070
+ disposition,
2071
+ 'disposition',
2072
+ errors,
2073
+ `$.contradictionDispositions[${index}]`,
2074
+ );
2075
+ if (!['resolved', 'unresolved'].includes(disposition.disposition)) {
2076
+ errors.push(
2077
+ issue(
2078
+ 'INVALID_CONTRADICTION_DISPOSITION',
2079
+ 'Contradiction disposition must be resolved or unresolved',
2080
+ `$.contradictionDispositions[${index}].disposition`,
2081
+ ),
2082
+ );
2083
+ }
2084
+ closedObject(
2085
+ disposition,
2086
+ new Set(['contradictionId', 'claimIds', 'disposition']),
2087
+ errors,
2088
+ `$.contradictionDispositions[${index}]`,
2089
+ );
2090
+ }
2091
+ }
2092
+ if (
2093
+ ['redundant-verification', 'contradiction-resolution'].includes(
2094
+ value.reviewKind,
2095
+ ) &&
2096
+ ((value.dispositions ?? []).length === 0 ||
2097
+ (value.permittedInputs ?? []).length === 0 ||
2098
+ (value.reviewKind === 'contradiction-resolution' &&
2099
+ (value.contradictionDispositions ?? []).length === 0))
2100
+ ) {
2101
+ errors.push(
2102
+ issue(
2103
+ 'EMPTY_ASSURANCE_RESULT',
2104
+ 'Thorough assurance results must bind immutable inputs, claims, and affected contradictions',
2105
+ '$',
2106
+ ),
2107
+ );
2108
+ }
2109
+ for (const [index, finding] of (value.coverageFindings ?? []).entries()) {
2110
+ for (const key of ['id', 'gapId', 'code', 'message']) {
2111
+ requiredString(finding, key, errors, `$.coverageFindings[${index}]`);
2112
+ }
2113
+ requiredArray(finding, 'claimIds', errors, `$.coverageFindings[${index}]`);
2114
+ if (typeof finding.material !== 'boolean') {
2115
+ errors.push(
2116
+ issue(
2117
+ 'MISSING_REQUIRED_FIELD',
2118
+ 'material must be a boolean',
2119
+ `$.coverageFindings[${index}].material`,
2120
+ ),
2121
+ );
2122
+ }
2123
+ closedObject(
2124
+ finding,
2125
+ new Set(['id', 'gapId', 'code', 'message', 'material', 'claimIds']),
2126
+ errors,
2127
+ `$.coverageFindings[${index}]`,
2128
+ );
2129
+ }
2130
+ if (
2131
+ value.reviewKind !== 'coverage' &&
2132
+ value.reviewKind !== 'reconciliation' &&
2133
+ (value.coverageFindings ?? []).length > 0
2134
+ ) {
2135
+ errors.push(
2136
+ issue(
2137
+ 'INVALID_COVERAGE_FINDING_OWNER',
2138
+ 'Only coverage results may report coverage findings',
2139
+ '$.coverageFindings',
2140
+ ),
2141
+ );
2142
+ }
2143
+ for (const [index, evidence] of (value.newEvidence ?? []).entries()) {
2144
+ closedObject(
2145
+ evidence,
2146
+ new Set([
2147
+ 'id',
2148
+ 'sourceId',
2149
+ 'locator',
2150
+ 'displayExcerpt',
2151
+ 'observedAt',
2152
+ 'contentHash',
2153
+ 'locatorValidation',
2154
+ 'provenance',
2155
+ 'redaction',
2156
+ ]),
2157
+ errors,
2158
+ `$.newEvidence[${index}]`,
2159
+ );
2160
+ for (const key of ['id', 'sourceId', 'displayExcerpt', 'observedAt']) {
2161
+ requiredString(evidence, key, errors, `$.newEvidence[${index}]`);
2162
+ }
2163
+ requiredObject(evidence, 'locator', errors, `$.newEvidence[${index}]`);
2164
+ requiredObject(
2165
+ evidence,
2166
+ 'locatorValidation',
2167
+ errors,
2168
+ `$.newEvidence[${index}]`,
2169
+ );
2170
+ validateExactReference(
2171
+ evidence.provenance,
2172
+ `$.newEvidence[${index}].provenance`,
2173
+ errors,
2174
+ );
2175
+ }
2176
+ if (
2177
+ (value.newEvidence ?? []).length > 0 ||
2178
+ value.evidenceAssociations !== undefined
2179
+ ) {
2180
+ requiredArray(value, 'evidenceAssociations', errors);
2181
+ }
2182
+ for (const [index, association] of (
2183
+ value.evidenceAssociations ?? []
2184
+ ).entries()) {
2185
+ for (const key of ['evidenceId', 'claimId', 'relation']) {
2186
+ requiredString(
2187
+ association,
2188
+ key,
2189
+ errors,
2190
+ `$.evidenceAssociations[${index}]`,
2191
+ );
2192
+ }
2193
+ if (
2194
+ !['supports', 'contradicts', 'qualifies', 'context'].includes(
2195
+ association.relation,
2196
+ )
2197
+ ) {
2198
+ errors.push(
2199
+ issue(
2200
+ 'INVALID_EVIDENCE_RELATION',
2201
+ 'Evidence association relation is not supported',
2202
+ `$.evidenceAssociations[${index}].relation`,
2203
+ ),
2204
+ );
2205
+ }
2206
+ closedObject(
2207
+ association,
2208
+ new Set(['evidenceId', 'claimId', 'relation']),
2209
+ errors,
2210
+ `$.evidenceAssociations[${index}]`,
2211
+ );
2212
+ }
2213
+ const common = [
2214
+ 'kind',
2215
+ 'schemaVersion',
2216
+ 'id',
2217
+ 'runId',
2218
+ 'reviewKind',
2219
+ 'reviewerLane',
2220
+ 'status',
2221
+ 'permittedInputs',
2222
+ 'excludedInputs',
2223
+ 'dispositions',
2224
+ 'newEvidence',
2225
+ 'evidenceAssociations',
2226
+ 'coverageFindings',
2227
+ 'unresolvedIssues',
2228
+ ];
2229
+ closedObject(
2230
+ value,
2231
+ new Set(
2232
+ value.reviewKind === 'reconciliation'
2233
+ ? [
2234
+ ...common,
2235
+ 'inputLedger',
2236
+ 'outputRevision',
2237
+ 'incorporatedReviewIds',
2238
+ 'transitions',
2239
+ 'additions',
2240
+ 'removals',
2241
+ 'removalDispositions',
2242
+ 'coverageDispositions',
2243
+ ]
2244
+ : value.reviewKind === 'contradiction-resolution'
2245
+ ? [...common, 'brief', 'contradictionDispositions']
2246
+ : [...common, 'brief'],
2247
+ ),
2248
+ errors,
2249
+ );
2250
+ }
2251
+
2252
+ function validateDispatchReceipt(value, errors) {
2253
+ for (const key of ['id', 'runId', 'stageId', 'laneId', 'state']) {
2254
+ requiredString(value, key, errors);
2255
+ }
2256
+ requiredObject(value, 'approvalProjection', errors);
2257
+ requiredString(value, 'approvalCanonicalJson', errors);
2258
+ requiredString(value, 'approvalFingerprint', errors);
2259
+ validateApprovalProjection(
2260
+ value.approvalProjection,
2261
+ errors,
2262
+ '$.approvalProjection',
2263
+ );
2264
+ if (
2265
+ !['prepared', 'approved', 'accepted', 'completed', 'failed'].includes(
2266
+ value.state,
2267
+ )
2268
+ ) {
2269
+ errors.push(
2270
+ issue('INVALID_DISPATCH_STATE', 'Unknown dispatch state', '$.state'),
2271
+ );
2272
+ }
2273
+ const approved = ['approved', 'accepted', 'completed', 'failed'].includes(
2274
+ value.state,
2275
+ );
2276
+ const launched = ['accepted', 'completed', 'failed'].includes(value.state);
2277
+ if (approved) {
2278
+ requiredTimestamp(value, 'approvedAt', errors);
2279
+ requiredObject(value, 'approvalEvidence', errors);
2280
+ validateApprovalEvidence(
2281
+ value.approvalEvidence,
2282
+ errors,
2283
+ '$.approvalEvidence',
2284
+ );
2285
+ } else if (value.approvedAt !== null || value.approvalEvidence !== null) {
2286
+ errors.push(
2287
+ issue(
2288
+ 'INVALID_DISPATCH_STATE_EVIDENCE',
2289
+ 'Prepared receipt cannot contain approval evidence',
2290
+ '$.approvalEvidence',
2291
+ ),
2292
+ );
2293
+ }
2294
+ if (launched) {
2295
+ requiredObject(value, 'catalogRecheck', errors);
2296
+ requiredObject(value, 'launchAcceptance', errors);
2297
+ validateCatalogRecheck(value.catalogRecheck, errors, '$.catalogRecheck');
2298
+ if (isObject(value.launchAcceptance)) {
2299
+ closedObject(
2300
+ value.launchAcceptance,
2301
+ new Set(['status', 'acceptedAt', 'handle']),
2302
+ errors,
2303
+ '$.launchAcceptance',
2304
+ );
2305
+ if (value.launchAcceptance.status !== 'accepted') {
2306
+ errors.push(
2307
+ issue(
2308
+ 'INVALID_LAUNCH_ACCEPTANCE',
2309
+ 'Launch was not accepted',
2310
+ '$.launchAcceptance.status',
2311
+ ),
2312
+ );
2313
+ }
2314
+ requiredTimestamp(
2315
+ value.launchAcceptance,
2316
+ 'acceptedAt',
2317
+ errors,
2318
+ '$.launchAcceptance',
2319
+ );
2320
+ requiredString(
2321
+ value.launchAcceptance,
2322
+ 'handle',
2323
+ errors,
2324
+ '$.launchAcceptance',
2325
+ );
2326
+ }
2327
+ } else if (value.catalogRecheck !== null || value.launchAcceptance !== null) {
2328
+ errors.push(
2329
+ issue(
2330
+ 'INVALID_DISPATCH_STATE_EVIDENCE',
2331
+ 'Pre-launch receipt cannot contain catalog or acceptance evidence',
2332
+ '$.catalogRecheck',
2333
+ ),
2334
+ );
2335
+ }
2336
+ if (value.state === 'completed') {
2337
+ requiredArray(value, 'artifactIds', errors);
2338
+ requiredObject(value, 'terminalOutcome', errors);
2339
+ if (isObject(value.terminalOutcome)) {
2340
+ closedObject(
2341
+ value.terminalOutcome,
2342
+ new Set(['status', 'completedAt']),
2343
+ errors,
2344
+ '$.terminalOutcome',
2345
+ );
2346
+ if (value.terminalOutcome.status !== 'completed') {
2347
+ errors.push(
2348
+ issue(
2349
+ 'INVALID_TERMINAL_OUTCOME',
2350
+ 'Terminal outcome is not completed',
2351
+ '$.terminalOutcome.status',
2352
+ ),
2353
+ );
2354
+ }
2355
+ requiredTimestamp(
2356
+ value.terminalOutcome,
2357
+ 'completedAt',
2358
+ errors,
2359
+ '$.terminalOutcome',
2360
+ );
2361
+ }
2362
+ } else if (value.terminalOutcome !== null) {
2363
+ errors.push(
2364
+ issue(
2365
+ 'INVALID_DISPATCH_STATE_EVIDENCE',
2366
+ 'Non-completed receipt cannot contain a terminal outcome',
2367
+ '$.terminalOutcome',
2368
+ ),
2369
+ );
2370
+ }
2371
+ closedObject(
2372
+ value,
2373
+ new Set([
2374
+ 'kind',
2375
+ 'schemaVersion',
2376
+ 'id',
2377
+ 'runId',
2378
+ 'stageId',
2379
+ 'laneId',
2380
+ 'state',
2381
+ 'approvalProjection',
2382
+ 'approvalCanonicalJson',
2383
+ 'approvalFingerprint',
2384
+ 'approvedAt',
2385
+ 'approvalEvidence',
2386
+ 'catalogRecheck',
2387
+ 'launchAcceptance',
2388
+ 'terminalOutcome',
2389
+ 'artifactIds',
2390
+ ]),
2391
+ errors,
2392
+ );
2393
+ }
2394
+
2395
+ function validateStageResult(value, errors) {
2396
+ for (const key of ['id', 'waveId', 'mode', 'laneId', 'status'])
2397
+ requiredString(value, key, errors);
2398
+ requiredArray(value, 'artifactIds', errors);
2399
+ requiredArray(value, 'dispatchReceiptIds', errors);
2400
+ if (!stageModes.includes(value.mode)) {
2401
+ errors.push(issue('INVALID_STAGE_MODE', 'Unknown stage mode', '$.mode'));
2402
+ }
2403
+ if (!['complete', 'failed', 'omitted'].includes(value.status)) {
2404
+ errors.push(
2405
+ issue('INVALID_STAGE_STATUS', 'Unknown stage status', '$.status'),
2406
+ );
2407
+ }
2408
+ closedObject(
2409
+ value,
2410
+ new Set([
2411
+ 'kind',
2412
+ 'schemaVersion',
2413
+ 'id',
2414
+ 'waveId',
2415
+ 'mode',
2416
+ 'laneId',
2417
+ 'status',
2418
+ 'artifactIds',
2419
+ 'dispatchReceiptIds',
2420
+ 'message',
2421
+ ]),
2422
+ errors,
2423
+ );
2424
+ }
2425
+
2426
+ export function validateArtifactShape(value) {
2427
+ const errors = [];
2428
+ if (!isObject(value)) {
2429
+ return {
2430
+ valid: false,
2431
+ errors: [issue('INVALID_ARTIFACT', 'Expected a JSON object')],
2432
+ };
2433
+ }
2434
+ if (!artifactKinds.has(value.kind)) {
2435
+ errors.push(
2436
+ issue(
2437
+ 'UNKNOWN_ARTIFACT_KIND',
2438
+ `Unknown artifact kind ${value.kind}`,
2439
+ '$.kind',
2440
+ ),
2441
+ );
2442
+ }
2443
+ if (value.schemaVersion !== SCHEMA_VERSION) {
2444
+ errors.push(
2445
+ issue(
2446
+ 'UNSUPPORTED_SCHEMA_VERSION',
2447
+ `Expected schemaVersion ${SCHEMA_VERSION}`,
2448
+ '$.schemaVersion',
2449
+ ),
2450
+ );
2451
+ }
2452
+ if (value.kind === 'recon.packet-manifest') validateManifest(value, errors);
2453
+ if (value.kind === 'recon.claim-ledger') validateLedger(value, errors);
2454
+ if (value.kind === 'recon.raw-dossier') validateDossier(value, errors);
2455
+ if (value.kind === 'recon.review-brief')
2456
+ validateReviewBriefArtifact(value, errors);
2457
+ if (value.kind === 'recon.review-result') validateReviewResult(value, errors);
2458
+ if (value.kind === 'recon.dispatch-receipt')
2459
+ validateDispatchReceipt(value, errors);
2460
+ if (value.kind === 'recon.stage-result') validateStageResult(value, errors);
2461
+ return { valid: errors.length === 0, errors };
2462
+ }