@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,1969 @@
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
+ ]);
10
+
11
+ export const profiles = ['quick', 'standard', 'thorough'];
12
+ export const claimStates = [
13
+ 'provisional',
14
+ 'supported',
15
+ 'verified',
16
+ 'contested',
17
+ 'unresolved',
18
+ 'unsupported',
19
+ ];
20
+ export const locatorStates = ['exact', 'redacted-exact', 'stale', 'invalid'];
21
+ export const sourceValidationStates = [
22
+ 'pinned',
23
+ 'unpinned',
24
+ 'stale',
25
+ 'invalid',
26
+ 'unavailable',
27
+ ];
28
+ export const workerModes = [
29
+ 'map',
30
+ 'gather',
31
+ 'compile',
32
+ 'verify',
33
+ 'adversary',
34
+ 'coverage',
35
+ 'reconcile',
36
+ ];
37
+ export const evidenceRelations = [
38
+ 'supports',
39
+ 'contradicts',
40
+ 'qualifies',
41
+ 'context',
42
+ ];
43
+ export const authorityLevels = ['provider-enforced', 'contract-enforced'];
44
+ export const waveModes = [
45
+ 'map',
46
+ 'gather',
47
+ 'compile',
48
+ 'semantic-verification',
49
+ 'adversarial',
50
+ 'coverage',
51
+ 'reconciliation',
52
+ 'redundant-gather',
53
+ 'redundant-verification',
54
+ 'contradiction-resolution',
55
+ ];
56
+ export const taskClasses = [
57
+ 'mechanical-recon',
58
+ 'intelligent-recon',
59
+ 'default-implementation',
60
+ 'hard-reasoning',
61
+ 'consequential',
62
+ ];
63
+
64
+ const legalTransitions = new Set([
65
+ 'provisional:supported',
66
+ 'provisional:verified',
67
+ 'provisional:contested',
68
+ 'provisional:unresolved',
69
+ 'provisional:unsupported',
70
+ 'supported:verified',
71
+ 'supported:contested',
72
+ 'supported:unresolved',
73
+ 'supported:unsupported',
74
+ 'verified:contested',
75
+ 'verified:unresolved',
76
+ 'verified:unsupported',
77
+ 'contested:supported',
78
+ 'contested:verified',
79
+ 'contested:unresolved',
80
+ 'contested:unsupported',
81
+ 'unresolved:supported',
82
+ 'unresolved:verified',
83
+ 'unresolved:contested',
84
+ 'unresolved:unsupported',
85
+ 'unsupported:provisional',
86
+ ]);
87
+
88
+ export function issue(code, message, path = '$', severity = 'error') {
89
+ return { code, message, path, severity };
90
+ }
91
+
92
+ export function isObject(value) {
93
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
94
+ }
95
+
96
+ export function isDigest(value) {
97
+ return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
98
+ }
99
+
100
+ export function validateArtifactReference(reference, path = '$') {
101
+ const errors = [];
102
+ if (!isObject(reference)) {
103
+ return [issue('INVALID_ARTIFACT_REFERENCE', 'Expected an object', path)];
104
+ }
105
+ if (typeof reference.path !== 'string' || reference.path.length === 0) {
106
+ errors.push(
107
+ issue(
108
+ 'INVALID_ARTIFACT_REFERENCE',
109
+ 'Artifact path must be a non-empty string',
110
+ `${path}.path`,
111
+ ),
112
+ );
113
+ }
114
+ if (!isDigest(reference.digest)) {
115
+ errors.push(
116
+ issue(
117
+ 'INVALID_ARTIFACT_REFERENCE',
118
+ 'Artifact digest must be sha256:<64 lowercase hex characters>',
119
+ `${path}.digest`,
120
+ ),
121
+ );
122
+ }
123
+ return errors;
124
+ }
125
+
126
+ function validateExactReference(reference, path, errors, extra = []) {
127
+ errors.push(...validateArtifactReference(reference, path));
128
+ closedObject(reference, new Set(['path', 'digest', ...extra]), errors, path);
129
+ }
130
+
131
+ function requiredString(value, key, errors, path = '$') {
132
+ if (typeof value?.[key] !== 'string' || value[key].length === 0) {
133
+ errors.push(
134
+ issue(
135
+ 'MISSING_REQUIRED_FIELD',
136
+ `${key} must be a non-empty string`,
137
+ `${path}.${key}`,
138
+ ),
139
+ );
140
+ }
141
+ }
142
+
143
+ function requiredTimestamp(value, key, errors, path = '$') {
144
+ requiredString(value, key, errors, path);
145
+ if (
146
+ typeof value?.[key] === 'string' &&
147
+ !Number.isFinite(Date.parse(value[key]))
148
+ ) {
149
+ errors.push(
150
+ issue(
151
+ 'INVALID_TIMESTAMP',
152
+ `${key} must be a parseable timestamp`,
153
+ `${path}.${key}`,
154
+ ),
155
+ );
156
+ }
157
+ }
158
+
159
+ function requiredInteger(value, key, errors, path = '$', minimum = 0) {
160
+ if (!Number.isInteger(value?.[key]) || value[key] < minimum) {
161
+ errors.push(
162
+ issue(
163
+ 'MISSING_REQUIRED_FIELD',
164
+ `${key} must be an integer greater than or equal to ${minimum}`,
165
+ `${path}.${key}`,
166
+ ),
167
+ );
168
+ }
169
+ }
170
+
171
+ function requiredArray(value, key, errors, path = '$') {
172
+ if (!Array.isArray(value?.[key])) {
173
+ errors.push(
174
+ issue(
175
+ 'MISSING_REQUIRED_FIELD',
176
+ `${key} must be an array`,
177
+ `${path}.${key}`,
178
+ ),
179
+ );
180
+ }
181
+ }
182
+
183
+ function requiredObject(value, key, errors, path = '$') {
184
+ if (!isObject(value?.[key])) {
185
+ errors.push(
186
+ issue(
187
+ 'MISSING_REQUIRED_FIELD',
188
+ `${key} must be an object`,
189
+ `${path}.${key}`,
190
+ ),
191
+ );
192
+ }
193
+ }
194
+
195
+ function closedObject(value, allowed, errors, path = '$') {
196
+ if (!isObject(value)) return;
197
+ for (const key of Object.keys(value)) {
198
+ if (!allowed.has(key)) {
199
+ errors.push(
200
+ issue('UNKNOWN_FIELD', `Unexpected field ${key}`, `${path}.${key}`),
201
+ );
202
+ }
203
+ }
204
+ }
205
+
206
+ function duplicateIds(values, path, errors) {
207
+ const seen = new Set();
208
+ for (const [index, value] of values.entries()) {
209
+ if (!isObject(value) || typeof value.id !== 'string') continue;
210
+ if (seen.has(value.id)) {
211
+ errors.push(
212
+ issue(
213
+ 'DUPLICATE_ID',
214
+ `Duplicate identifier ${value.id}`,
215
+ `${path}[${index}].id`,
216
+ ),
217
+ );
218
+ }
219
+ seen.add(value.id);
220
+ }
221
+ }
222
+
223
+ const executionKeys = new Set([
224
+ 'provider',
225
+ 'route',
226
+ 'role',
227
+ 'model',
228
+ 'effort',
229
+ 'reasoningMode',
230
+ 'serviceTier',
231
+ 'authority',
232
+ 'maxConcurrency',
233
+ 'deadlineSeconds',
234
+ 'retryLimit',
235
+ 'waves',
236
+ 'approval',
237
+ ]);
238
+
239
+ export function approvalFingerprintInput(execution) {
240
+ const { approval: _approval, ...approved } = execution;
241
+ return approved;
242
+ }
243
+
244
+ function validateApprovalEvidence(value, errors, path) {
245
+ if (!isObject(value)) return;
246
+ closedObject(
247
+ value,
248
+ new Set(['type', 'approvedAt', 'fingerprint']),
249
+ errors,
250
+ path,
251
+ );
252
+ if (value.type !== 'explicit-user-approval') {
253
+ errors.push(
254
+ issue(
255
+ 'INVALID_APPROVAL_EVIDENCE',
256
+ 'Approval evidence must record explicit user approval',
257
+ `${path}.type`,
258
+ ),
259
+ );
260
+ }
261
+ requiredTimestamp(value, 'approvedAt', errors, path);
262
+ if (!isDigest(value.fingerprint)) {
263
+ errors.push(
264
+ issue(
265
+ 'INVALID_APPROVAL_FINGERPRINT',
266
+ 'Approval evidence fingerprint must be sha256',
267
+ `${path}.fingerprint`,
268
+ ),
269
+ );
270
+ }
271
+ }
272
+
273
+ export function validateExecution(value, errors, path = '$.execution') {
274
+ if (!isObject(value)) return;
275
+ closedObject(value, executionKeys, errors, path);
276
+ for (const key of ['provider', 'route', 'role', 'model', 'effort']) {
277
+ requiredString(value, key, errors, path);
278
+ }
279
+ for (const key of ['reasoningMode', 'serviceTier']) {
280
+ if (value[key] !== null && typeof value[key] !== 'string') {
281
+ errors.push(
282
+ issue(
283
+ 'MISSING_REQUIRED_FIELD',
284
+ `${key} must be a string or null`,
285
+ `${path}.${key}`,
286
+ ),
287
+ );
288
+ }
289
+ }
290
+ if (!authorityLevels.includes(value.authority)) {
291
+ errors.push(
292
+ issue(
293
+ 'INVALID_AUTHORITY_LEVEL',
294
+ 'Approved authority must be provider-enforced or contract-enforced',
295
+ `${path}.authority`,
296
+ ),
297
+ );
298
+ }
299
+ requiredInteger(value, 'maxConcurrency', errors, path, 1);
300
+ requiredInteger(value, 'deadlineSeconds', errors, path, 1);
301
+ requiredInteger(value, 'retryLimit', errors, path, 0);
302
+ requiredArray(value, 'waves', errors, path);
303
+ requiredObject(value, 'approval', errors, path);
304
+ validateApprovalEvidence(value.approval, errors, `${path}.approval`);
305
+ const waveIds = new Set();
306
+ const laneIds = new Set();
307
+ for (const [waveIndex, wave] of (value.waves ?? []).entries()) {
308
+ const wavePath = `${path}.waves[${waveIndex}]`;
309
+ closedObject(
310
+ wave,
311
+ new Set(['waveId', 'mode', 'taskClass', 'lanes', 'conditional']),
312
+ errors,
313
+ wavePath,
314
+ );
315
+ requiredString(wave, 'waveId', errors, wavePath);
316
+ requiredArray(wave, 'lanes', errors, wavePath);
317
+ if (Array.isArray(wave?.lanes) && wave.lanes.length === 0) {
318
+ errors.push(
319
+ issue(
320
+ 'MISSING_REQUIRED_FIELD',
321
+ 'Every approved wave needs at least one lane',
322
+ `${wavePath}.lanes`,
323
+ ),
324
+ );
325
+ }
326
+ if (!waveModes.includes(wave?.mode)) {
327
+ errors.push(
328
+ issue('INVALID_WAVE_MODE', 'Unknown wave mode', `${wavePath}.mode`),
329
+ );
330
+ }
331
+ if (!taskClasses.includes(wave?.taskClass)) {
332
+ errors.push(
333
+ issue(
334
+ 'INVALID_TASK_CLASS',
335
+ 'Unknown wave task class',
336
+ `${wavePath}.taskClass`,
337
+ ),
338
+ );
339
+ }
340
+ if (typeof wave?.conditional !== 'boolean') {
341
+ errors.push(
342
+ issue(
343
+ 'MISSING_REQUIRED_FIELD',
344
+ 'conditional must be a boolean',
345
+ `${wavePath}.conditional`,
346
+ ),
347
+ );
348
+ }
349
+ if (waveIds.has(wave?.waveId)) {
350
+ errors.push(
351
+ issue(
352
+ 'DUPLICATE_ID',
353
+ `Duplicate wave ${wave.waveId}`,
354
+ `${wavePath}.waveId`,
355
+ ),
356
+ );
357
+ }
358
+ waveIds.add(wave?.waveId);
359
+ for (const [laneIndex, lane] of (Array.isArray(wave?.lanes)
360
+ ? wave.lanes
361
+ : []
362
+ ).entries()) {
363
+ const lanePath = `${wavePath}.lanes[${laneIndex}]`;
364
+ closedObject(
365
+ lane,
366
+ new Set(['laneId', 'scope', 'writeRoot']),
367
+ errors,
368
+ lanePath,
369
+ );
370
+ for (const key of ['laneId', 'scope', 'writeRoot']) {
371
+ requiredString(lane, key, errors, lanePath);
372
+ }
373
+ if (
374
+ typeof lane?.writeRoot === 'string' &&
375
+ (lane.writeRoot.startsWith('/') ||
376
+ lane.writeRoot.split('/').some((segment) => segment === '..'))
377
+ ) {
378
+ errors.push(
379
+ issue(
380
+ 'INVALID_WRITE_ROOT',
381
+ 'Lane write root must be a packet-relative path',
382
+ `${lanePath}.writeRoot`,
383
+ ),
384
+ );
385
+ }
386
+ if (laneIds.has(lane?.laneId)) {
387
+ errors.push(
388
+ issue(
389
+ 'DUPLICATE_ID',
390
+ `Duplicate lane ${lane.laneId}`,
391
+ `${lanePath}.laneId`,
392
+ ),
393
+ );
394
+ }
395
+ laneIds.add(lane?.laneId);
396
+ }
397
+ }
398
+ }
399
+
400
+ function validateManifest(value, errors) {
401
+ closedObject(
402
+ value,
403
+ new Set([
404
+ 'kind',
405
+ 'schemaVersion',
406
+ 'run',
407
+ 'request',
408
+ 'sources',
409
+ 'execution',
410
+ 'artifacts',
411
+ 'gaps',
412
+ ]),
413
+ errors,
414
+ );
415
+ if (!isObject(value.run)) {
416
+ errors.push(
417
+ issue('MISSING_REQUIRED_FIELD', 'run must be an object', '$.run'),
418
+ );
419
+ } else {
420
+ closedObject(
421
+ value.run,
422
+ new Set([
423
+ 'id',
424
+ 'topic',
425
+ 'status',
426
+ 'requestedProfile',
427
+ 'achievedProfile',
428
+ 'createdAt',
429
+ 'updatedAt',
430
+ ]),
431
+ errors,
432
+ '$.run',
433
+ );
434
+ requiredString(value.run, 'id', errors, '$.run');
435
+ requiredString(value.run, 'topic', errors, '$.run');
436
+ if (
437
+ ![
438
+ 'preparing',
439
+ 'awaiting-approval',
440
+ 'running',
441
+ 'complete',
442
+ 'partial',
443
+ 'failed',
444
+ ].includes(value.run.status)
445
+ ) {
446
+ errors.push(
447
+ issue('INVALID_RUN_STATUS', 'Unknown run status', '$.run.status'),
448
+ );
449
+ }
450
+ if (!profiles.includes(value.run.requestedProfile)) {
451
+ errors.push(
452
+ issue(
453
+ 'INVALID_PROFILE',
454
+ 'Unknown requested profile',
455
+ '$.run.requestedProfile',
456
+ ),
457
+ );
458
+ }
459
+ if (
460
+ value.run.achievedProfile !== null &&
461
+ !profiles.includes(value.run.achievedProfile)
462
+ ) {
463
+ errors.push(
464
+ issue(
465
+ 'INVALID_PROFILE',
466
+ 'Unknown achieved profile',
467
+ '$.run.achievedProfile',
468
+ ),
469
+ );
470
+ }
471
+ }
472
+ requiredArray(value, 'sources', errors);
473
+ requiredArray(value, 'artifacts', errors);
474
+ requiredArray(value, 'gaps', errors);
475
+ requiredObject(value, 'request', errors);
476
+ requiredObject(value, 'execution', errors);
477
+ if (isObject(value.request)) {
478
+ closedObject(
479
+ value.request,
480
+ new Set([
481
+ 'objective',
482
+ 'questions',
483
+ 'includedScope',
484
+ 'excludedScope',
485
+ 'contextReferences',
486
+ 'outputPath',
487
+ ]),
488
+ errors,
489
+ '$.request',
490
+ );
491
+ requiredString(value.request, 'objective', errors, '$.request');
492
+ requiredString(value.request, 'outputPath', errors, '$.request');
493
+ for (const key of [
494
+ 'questions',
495
+ 'includedScope',
496
+ 'excludedScope',
497
+ 'contextReferences',
498
+ ]) {
499
+ requiredArray(value.request, key, errors, '$.request');
500
+ }
501
+ }
502
+ validateExecution(value.execution, errors, '$.execution');
503
+ if (Array.isArray(value.sources))
504
+ duplicateIds(value.sources, '$.sources', errors);
505
+ if (Array.isArray(value.gaps)) duplicateIds(value.gaps, '$.gaps', errors);
506
+ for (const [index, reference] of (value.artifacts ?? []).entries()) {
507
+ validateExactReference(reference, `$.artifacts[${index}]`, errors);
508
+ }
509
+ for (const [index, source] of (value.sources ?? []).entries()) {
510
+ for (const key of [
511
+ 'kind',
512
+ 'id',
513
+ 'authority',
514
+ 'observedAt',
515
+ 'validationState',
516
+ ]) {
517
+ requiredString(source, key, errors, `$.sources[${index}]`);
518
+ }
519
+ if (typeof source.available !== 'boolean') {
520
+ errors.push(
521
+ issue(
522
+ 'MISSING_REQUIRED_FIELD',
523
+ 'available must be a boolean',
524
+ `$.sources[${index}].available`,
525
+ ),
526
+ );
527
+ }
528
+ if (!sourceValidationStates.includes(source.validationState)) {
529
+ errors.push(
530
+ issue(
531
+ 'INVALID_SOURCE_VALIDATION_STATE',
532
+ 'Unknown source validation state',
533
+ `$.sources[${index}].validationState`,
534
+ ),
535
+ );
536
+ }
537
+ if (
538
+ (source.available === false) !==
539
+ (source.validationState === 'unavailable')
540
+ ) {
541
+ errors.push(
542
+ issue(
543
+ 'SOURCE_AVAILABILITY_MISMATCH',
544
+ 'Unavailable sources must use unavailable validation state and vice versa',
545
+ `$.sources[${index}]`,
546
+ ),
547
+ );
548
+ }
549
+ const common = [
550
+ 'kind',
551
+ 'id',
552
+ 'available',
553
+ 'authority',
554
+ 'observedAt',
555
+ 'validationState',
556
+ ];
557
+ const kindKeys = {
558
+ repository: ['root', 'revision', 'dirty', 'contentHashes'],
559
+ file: ['path', 'contentHash'],
560
+ url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
561
+ 'command-output': [
562
+ 'argv',
563
+ 'cwd',
564
+ 'exitStatus',
565
+ 'outputPath',
566
+ 'outputDigest',
567
+ 'environmentNames',
568
+ ],
569
+ 'connected-resource': [
570
+ 'system',
571
+ 'resourceId',
572
+ 'resourceVersion',
573
+ 'retrievalToken',
574
+ 'capturePath',
575
+ 'captureDigest',
576
+ ],
577
+ }[source.kind];
578
+ if (!kindKeys) {
579
+ errors.push(
580
+ issue(
581
+ 'INVALID_SOURCE_KIND',
582
+ 'Unknown source kind',
583
+ `$.sources[${index}].kind`,
584
+ ),
585
+ );
586
+ } else {
587
+ closedObject(
588
+ source,
589
+ new Set([...common, ...kindKeys]),
590
+ errors,
591
+ `$.sources[${index}]`,
592
+ );
593
+ if (source.kind === 'url' && isObject(source.validatorState)) {
594
+ closedObject(
595
+ source.validatorState,
596
+ new Set(['etag', 'lastModified', 'capturePath', 'captureDigest']),
597
+ errors,
598
+ `$.sources[${index}].validatorState`,
599
+ );
600
+ const hasDirectCapture =
601
+ Object.hasOwn(source, 'capturePath') ||
602
+ Object.hasOwn(source, 'captureDigest');
603
+ const hasValidatorCapture =
604
+ Object.hasOwn(source.validatorState, 'capturePath') ||
605
+ Object.hasOwn(source.validatorState, 'captureDigest');
606
+ if (hasDirectCapture && hasValidatorCapture) {
607
+ errors.push(
608
+ issue(
609
+ 'DUAL_URL_CAPTURE',
610
+ 'URL sources must declare exactly one capture representation',
611
+ `$.sources[${index}]`,
612
+ ),
613
+ );
614
+ }
615
+ }
616
+ }
617
+ }
618
+ for (const [index, gap] of (value.gaps ?? []).entries()) {
619
+ requiredString(gap, 'id', errors, `$.gaps[${index}]`);
620
+ requiredString(gap, 'code', errors, `$.gaps[${index}]`);
621
+ requiredString(gap, 'message', errors, `$.gaps[${index}]`);
622
+ if (typeof gap.material !== 'boolean') {
623
+ errors.push(
624
+ issue(
625
+ 'MISSING_REQUIRED_FIELD',
626
+ 'material must be a boolean',
627
+ `$.gaps[${index}].material`,
628
+ ),
629
+ );
630
+ }
631
+ closedObject(
632
+ gap,
633
+ new Set([
634
+ 'id',
635
+ 'code',
636
+ 'message',
637
+ 'material',
638
+ 'sourceIds',
639
+ 'claimIds',
640
+ 'coverageFindingIds',
641
+ ]),
642
+ errors,
643
+ `$.gaps[${index}]`,
644
+ );
645
+ }
646
+ }
647
+
648
+ function validateLedger(value, errors) {
649
+ closedObject(
650
+ value,
651
+ new Set([
652
+ 'kind',
653
+ 'schemaVersion',
654
+ 'runId',
655
+ 'revision',
656
+ 'inputArtifacts',
657
+ 'synthesis',
658
+ 'evidence',
659
+ 'claims',
660
+ 'unresolvedQuestions',
661
+ 'transitions',
662
+ ]),
663
+ errors,
664
+ );
665
+ requiredString(value, 'runId', errors);
666
+ if (!Number.isInteger(value.revision) || value.revision < 1) {
667
+ errors.push(
668
+ issue(
669
+ 'INVALID_REVISION',
670
+ 'revision must be a positive integer',
671
+ '$.revision',
672
+ ),
673
+ );
674
+ }
675
+ for (const key of [
676
+ 'inputArtifacts',
677
+ 'evidence',
678
+ 'claims',
679
+ 'unresolvedQuestions',
680
+ ]) {
681
+ requiredArray(value, key, errors);
682
+ }
683
+ requiredArray(value, 'transitions', errors);
684
+ requiredObject(value, 'synthesis', errors);
685
+ if (isObject(value.synthesis)) {
686
+ requiredString(value.synthesis, 'answer', errors, '$.synthesis');
687
+ for (const key of ['keyClaimIds', 'caveats', 'unresolvedQuestionIds']) {
688
+ requiredArray(value.synthesis, key, errors, '$.synthesis');
689
+ }
690
+ closedObject(
691
+ value.synthesis,
692
+ new Set(['answer', 'keyClaimIds', 'caveats', 'unresolvedQuestionIds']),
693
+ errors,
694
+ '$.synthesis',
695
+ );
696
+ const claimIds = new Set(
697
+ Array.isArray(value.claims)
698
+ ? value.claims.map((claim) => claim?.id).filter(Boolean)
699
+ : [],
700
+ );
701
+ if (Array.isArray(value.synthesis.keyClaimIds)) {
702
+ for (const [index, claimId] of value.synthesis.keyClaimIds.entries()) {
703
+ if (typeof claimId !== 'string' || claimId.trim().length === 0) {
704
+ errors.push(
705
+ issue(
706
+ 'INVALID_ARRAY_ENTRY',
707
+ 'keyClaimIds entries must be non-empty strings',
708
+ `$.synthesis.keyClaimIds[${index}]`,
709
+ ),
710
+ );
711
+ } else if (!claimIds.has(claimId)) {
712
+ errors.push(
713
+ issue(
714
+ 'SYNTHESIS_REFERENCE_MISSING',
715
+ `Synthesis key claim ${claimId} does not resolve to an existing claim`,
716
+ `$.synthesis.keyClaimIds[${index}]`,
717
+ ),
718
+ );
719
+ }
720
+ }
721
+ }
722
+ const questionIds = new Set(
723
+ Array.isArray(value.unresolvedQuestions)
724
+ ? value.unresolvedQuestions.map((q) => q?.id).filter(Boolean)
725
+ : [],
726
+ );
727
+ if (Array.isArray(value.synthesis.unresolvedQuestionIds)) {
728
+ for (const [
729
+ index,
730
+ questionId,
731
+ ] of value.synthesis.unresolvedQuestionIds.entries()) {
732
+ if (typeof questionId !== 'string' || questionId.trim().length === 0) {
733
+ errors.push(
734
+ issue(
735
+ 'INVALID_ARRAY_ENTRY',
736
+ 'unresolvedQuestionIds entries must be non-empty strings',
737
+ `$.synthesis.unresolvedQuestionIds[${index}]`,
738
+ ),
739
+ );
740
+ } else if (!questionIds.has(questionId)) {
741
+ errors.push(
742
+ issue(
743
+ 'SYNTHESIS_REFERENCE_MISSING',
744
+ `Synthesis unresolved question ${questionId} does not resolve to an existing question`,
745
+ `$.synthesis.unresolvedQuestionIds[${index}]`,
746
+ ),
747
+ );
748
+ }
749
+ }
750
+ }
751
+ }
752
+ for (const key of ['evidence', 'claims', 'unresolvedQuestions']) {
753
+ if (Array.isArray(value[key])) duplicateIds(value[key], `$.${key}`, errors);
754
+ }
755
+ const evidenceIds = new Set(
756
+ Array.isArray(value.evidence)
757
+ ? value.evidence.map((item) => item?.id).filter(Boolean)
758
+ : [],
759
+ );
760
+ for (const [index, reference] of (value.inputArtifacts ?? []).entries()) {
761
+ validateExactReference(reference, `$.inputArtifacts[${index}]`, errors);
762
+ }
763
+ for (const [index, evidence] of (value.evidence ?? []).entries()) {
764
+ closedObject(
765
+ evidence,
766
+ new Set([
767
+ 'id',
768
+ 'sourceId',
769
+ 'locator',
770
+ 'displayExcerpt',
771
+ 'observedAt',
772
+ 'contentHash',
773
+ 'locatorValidation',
774
+ 'provenance',
775
+ 'redaction',
776
+ ]),
777
+ errors,
778
+ `$.evidence[${index}]`,
779
+ );
780
+ requiredString(evidence, 'id', errors, `$.evidence[${index}]`);
781
+ requiredString(evidence, 'sourceId', errors, `$.evidence[${index}]`);
782
+ requiredString(evidence, 'displayExcerpt', errors, `$.evidence[${index}]`);
783
+ requiredString(evidence, 'observedAt', errors, `$.evidence[${index}]`);
784
+ if (!isObject(evidence.locator)) {
785
+ errors.push(
786
+ issue(
787
+ 'INVALID_LOCATOR',
788
+ 'locator must be an object',
789
+ `$.evidence[${index}].locator`,
790
+ ),
791
+ );
792
+ } else {
793
+ const locatorKeys = {
794
+ repository: ['kind', 'path', 'revision', 'lineStart', 'lineEnd'],
795
+ file: ['kind', 'path', 'lineStart', 'lineEnd'],
796
+ url: ['kind', 'url', 'retrievedAt', 'fragment', 'validatorToken'],
797
+ 'command-output': [
798
+ 'kind',
799
+ 'artifactPath',
800
+ 'lineStart',
801
+ 'lineEnd',
802
+ 'commandDigest',
803
+ ],
804
+ 'connected-resource': [
805
+ 'kind',
806
+ 'system',
807
+ 'resourceId',
808
+ 'resourceVersion',
809
+ 'retrievalToken',
810
+ 'fieldOrSection',
811
+ 'retrievedAt',
812
+ ],
813
+ }[evidence.locator.kind];
814
+ if (!locatorKeys) {
815
+ errors.push(
816
+ issue(
817
+ 'INVALID_LOCATOR',
818
+ 'Unknown locator kind',
819
+ `$.evidence[${index}].locator.kind`,
820
+ ),
821
+ );
822
+ } else {
823
+ closedObject(
824
+ evidence.locator,
825
+ new Set(locatorKeys),
826
+ errors,
827
+ `$.evidence[${index}].locator`,
828
+ );
829
+ }
830
+ }
831
+ if (!locatorStates.includes(evidence.locatorValidation?.status)) {
832
+ errors.push(
833
+ issue(
834
+ 'INVALID_LOCATOR_STATE',
835
+ 'Unknown locator validation state',
836
+ `$.evidence[${index}].locatorValidation.status`,
837
+ ),
838
+ );
839
+ }
840
+ if (isObject(evidence.locatorValidation)) {
841
+ closedObject(
842
+ evidence.locatorValidation,
843
+ new Set(['status', 'validatedAt']),
844
+ errors,
845
+ `$.evidence[${index}].locatorValidation`,
846
+ );
847
+ requiredString(
848
+ evidence.locatorValidation,
849
+ 'validatedAt',
850
+ errors,
851
+ `$.evidence[${index}].locatorValidation`,
852
+ );
853
+ }
854
+ validateExactReference(
855
+ evidence.provenance,
856
+ `$.evidence[${index}].provenance`,
857
+ errors,
858
+ );
859
+ }
860
+ for (const [index, claim] of (value.claims ?? []).entries()) {
861
+ closedObject(
862
+ claim,
863
+ new Set([
864
+ 'id',
865
+ 'statement',
866
+ 'status',
867
+ 'evidence',
868
+ 'qualifications',
869
+ 'reviewIds',
870
+ 'derivedFrom',
871
+ 'challenges',
872
+ ]),
873
+ errors,
874
+ `$.claims[${index}]`,
875
+ );
876
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
877
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
878
+ if (!claimStates.includes(claim.status)) {
879
+ errors.push(
880
+ issue(
881
+ 'INVALID_CLAIM_STATE',
882
+ 'Unknown claim state',
883
+ `$.claims[${index}].status`,
884
+ ),
885
+ );
886
+ }
887
+ for (const key of [
888
+ 'evidence',
889
+ 'qualifications',
890
+ 'reviewIds',
891
+ 'derivedFrom',
892
+ 'challenges',
893
+ ]) {
894
+ requiredArray(claim, key, errors, `$.claims[${index}]`);
895
+ }
896
+ const linkedEvidenceIds = new Set();
897
+ for (const [linkIndex, link] of (claim.evidence ?? []).entries()) {
898
+ const linkPath = `$.claims[${index}].evidence[${linkIndex}]`;
899
+ if (!isObject(link)) {
900
+ errors.push(
901
+ issue(
902
+ 'INVALID_EVIDENCE_LINK',
903
+ 'Claim evidence links must be { evidenceId, relation } objects',
904
+ linkPath,
905
+ ),
906
+ );
907
+ continue;
908
+ }
909
+ closedObject(link, new Set(['evidenceId', 'relation']), errors, linkPath);
910
+ requiredString(link, 'evidenceId', errors, linkPath);
911
+ if (!evidenceRelations.includes(link.relation)) {
912
+ errors.push(
913
+ issue(
914
+ 'INVALID_EVIDENCE_RELATION',
915
+ 'Claim evidence relation is not supported',
916
+ `${linkPath}.relation`,
917
+ ),
918
+ );
919
+ }
920
+ if (
921
+ typeof link.evidenceId === 'string' &&
922
+ !evidenceIds.has(link.evidenceId)
923
+ ) {
924
+ errors.push(
925
+ issue(
926
+ 'UNRESOLVED_EVIDENCE_LINK',
927
+ `Claim evidence link ${link.evidenceId} does not resolve in this ledger`,
928
+ `${linkPath}.evidenceId`,
929
+ ),
930
+ );
931
+ }
932
+ if (linkedEvidenceIds.has(link.evidenceId)) {
933
+ errors.push(
934
+ issue(
935
+ 'DUPLICATE_EVIDENCE_LINK',
936
+ `Claim links evidence ${link.evidenceId} more than once`,
937
+ linkPath,
938
+ ),
939
+ );
940
+ }
941
+ linkedEvidenceIds.add(link.evidenceId);
942
+ }
943
+ for (const [referenceIndex, reference] of (
944
+ claim.derivedFrom ?? []
945
+ ).entries()) {
946
+ validateExactReference(
947
+ reference,
948
+ `$.claims[${index}].derivedFrom[${referenceIndex}]`,
949
+ errors,
950
+ );
951
+ }
952
+ }
953
+ for (const [index, transition] of (value.transitions ?? []).entries()) {
954
+ closedObject(
955
+ transition,
956
+ new Set(['claimId', 'from', 'to']),
957
+ errors,
958
+ `$.transitions[${index}]`,
959
+ );
960
+ if (!legalTransitions.has(`${transition.from}:${transition.to}`)) {
961
+ errors.push(
962
+ issue(
963
+ 'ILLEGAL_CLAIM_TRANSITION',
964
+ `Illegal claim transition ${transition.from} -> ${transition.to}`,
965
+ `$.transitions[${index}]`,
966
+ ),
967
+ );
968
+ }
969
+ }
970
+ const claimIds = new Set((value.claims ?? []).map((claim) => claim.id));
971
+ const lastTransitionByClaim = new Map();
972
+ for (const transition of value.transitions ?? []) {
973
+ if (!claimIds.has(transition.claimId)) {
974
+ errors.push(
975
+ issue(
976
+ 'UNKNOWN_TRANSITION_CLAIM',
977
+ `Transition references unknown claim ${transition.claimId}`,
978
+ '$.transitions',
979
+ ),
980
+ );
981
+ }
982
+ lastTransitionByClaim.set(transition.claimId, transition);
983
+ }
984
+ for (const claim of value.claims ?? []) {
985
+ const lastTransition = lastTransitionByClaim.get(claim.id);
986
+ if (value.revision === 1 && claim.status === 'provisional') {
987
+ if (lastTransition) {
988
+ errors.push(
989
+ issue(
990
+ 'INVALID_PROVISIONAL_GENESIS',
991
+ `Revision-one provisional claim ${claim.id} must use genesis without an incoming transition`,
992
+ `claim:${claim.id}`,
993
+ ),
994
+ );
995
+ }
996
+ continue;
997
+ }
998
+ if (
999
+ (value.revision === 1 || lastTransition) &&
1000
+ lastTransition?.to !== claim.status
1001
+ ) {
1002
+ errors.push(
1003
+ issue(
1004
+ 'CLAIM_TRANSITION_MISMATCH',
1005
+ `Final transition does not establish ${claim.id} as ${claim.status}`,
1006
+ `claim:${claim.id}`,
1007
+ ),
1008
+ );
1009
+ }
1010
+ if (
1011
+ value.revision > 1 &&
1012
+ claim.status === 'provisional' &&
1013
+ lastTransition?.to !== 'provisional'
1014
+ ) {
1015
+ errors.push(
1016
+ issue(
1017
+ 'INVALID_PROVISIONAL_GENESIS',
1018
+ `Later revision provisional claim ${claim.id} requires a legal incoming transition`,
1019
+ `claim:${claim.id}`,
1020
+ ),
1021
+ );
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ function validateDossier(value, errors) {
1027
+ closedObject(
1028
+ value,
1029
+ new Set([
1030
+ 'kind',
1031
+ 'schemaVersion',
1032
+ 'id',
1033
+ 'runId',
1034
+ 'waveId',
1035
+ 'laneId',
1036
+ 'mode',
1037
+ 'outcome',
1038
+ 'allowedInputs',
1039
+ 'excludedInputs',
1040
+ 'findings',
1041
+ 'uncertainty',
1042
+ 'contradictions',
1043
+ 'gaps',
1044
+ ]),
1045
+ errors,
1046
+ );
1047
+ for (const key of ['runId', 'waveId', 'laneId', 'mode', 'outcome']) {
1048
+ requiredString(value, key, errors);
1049
+ }
1050
+ if (!workerModes.includes(value.mode)) {
1051
+ errors.push(issue('INVALID_WORKER_MODE', 'Unknown worker mode', '$.mode'));
1052
+ }
1053
+ if (!['complete', 'partial', 'failed'].includes(value.outcome)) {
1054
+ errors.push(
1055
+ issue('INVALID_DOSSIER_OUTCOME', 'Unknown dossier outcome', '$.outcome'),
1056
+ );
1057
+ }
1058
+ for (const key of [
1059
+ 'allowedInputs',
1060
+ 'excludedInputs',
1061
+ 'findings',
1062
+ 'uncertainty',
1063
+ 'contradictions',
1064
+ 'gaps',
1065
+ ]) {
1066
+ requiredArray(value, key, errors);
1067
+ }
1068
+ }
1069
+
1070
+ const reviewSourceKindFields = {
1071
+ repository: ['root', 'revision', 'dirty', 'contentHashes'],
1072
+ file: ['path', 'contentHash'],
1073
+ url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
1074
+ 'command-output': [
1075
+ 'argv',
1076
+ 'cwd',
1077
+ 'exitStatus',
1078
+ 'outputPath',
1079
+ 'outputDigest',
1080
+ 'environmentNames',
1081
+ ],
1082
+ 'connected-resource': [
1083
+ 'system',
1084
+ 'resourceId',
1085
+ 'resourceVersion',
1086
+ 'retrievalToken',
1087
+ 'capturePath',
1088
+ 'captureDigest',
1089
+ ],
1090
+ };
1091
+ const commonReviewSourceFields = [
1092
+ 'id',
1093
+ 'kind',
1094
+ 'available',
1095
+ 'authority',
1096
+ 'observedAt',
1097
+ 'validationState',
1098
+ ];
1099
+
1100
+ function validateReviewBriefSource(source, index, errors) {
1101
+ const path = `$.sources[${index}]`;
1102
+ if (!isObject(source)) {
1103
+ errors.push(
1104
+ issue(
1105
+ 'INVALID_SOURCE_DESCRIPTOR',
1106
+ 'Verification brief sources must be objects',
1107
+ path,
1108
+ ),
1109
+ );
1110
+ return;
1111
+ }
1112
+ for (const key of commonReviewSourceFields) {
1113
+ if (key === 'available') continue;
1114
+ requiredString(source, key, errors, path);
1115
+ }
1116
+ if (typeof source.available !== 'boolean') {
1117
+ errors.push(
1118
+ issue(
1119
+ 'MISSING_REQUIRED_FIELD',
1120
+ 'available must be a boolean',
1121
+ `${path}.available`,
1122
+ ),
1123
+ );
1124
+ }
1125
+ if (!sourceValidationStates.includes(source.validationState)) {
1126
+ errors.push(
1127
+ issue(
1128
+ 'INVALID_SOURCE_VALIDATION_STATE',
1129
+ 'Unknown source validation state',
1130
+ `${path}.validationState`,
1131
+ ),
1132
+ );
1133
+ }
1134
+ const kindFields = reviewSourceKindFields[source.kind];
1135
+ if (!kindFields) {
1136
+ errors.push(
1137
+ issue('INVALID_SOURCE_KIND', 'Unknown source kind', `${path}.kind`),
1138
+ );
1139
+ return;
1140
+ }
1141
+ closedObject(
1142
+ source,
1143
+ new Set([...commonReviewSourceFields, ...kindFields]),
1144
+ errors,
1145
+ path,
1146
+ );
1147
+
1148
+ if (source.kind === 'repository') {
1149
+ requiredString(source, 'root', errors, path);
1150
+ requiredString(source, 'revision', errors, path);
1151
+ requiredObject(source, 'contentHashes', errors, path);
1152
+ if (typeof source.dirty !== 'boolean') {
1153
+ errors.push(
1154
+ issue(
1155
+ 'MISSING_REQUIRED_FIELD',
1156
+ 'dirty must be a boolean',
1157
+ `${path}.dirty`,
1158
+ ),
1159
+ );
1160
+ }
1161
+ } else if (source.kind === 'file') {
1162
+ requiredString(source, 'path', errors, path);
1163
+ requiredString(source, 'contentHash', errors, path);
1164
+ if (!isDigest(source.contentHash)) {
1165
+ errors.push(
1166
+ issue(
1167
+ 'INVALID_SOURCE_DESCRIPTOR',
1168
+ 'contentHash must be a sha256 digest',
1169
+ `${path}.contentHash`,
1170
+ ),
1171
+ );
1172
+ }
1173
+ } else if (source.kind === 'url') {
1174
+ requiredString(source, 'url', errors, path);
1175
+ const hasDirectCaptureFields =
1176
+ Object.hasOwn(source, 'capturePath') ||
1177
+ Object.hasOwn(source, 'captureDigest');
1178
+ if (hasDirectCaptureFields) {
1179
+ requiredString(source, 'capturePath', errors, path);
1180
+ requiredString(source, 'captureDigest', errors, path);
1181
+ if (!isDigest(source.captureDigest)) {
1182
+ errors.push(
1183
+ issue(
1184
+ 'INVALID_SOURCE_DESCRIPTOR',
1185
+ 'captureDigest must be a sha256 digest',
1186
+ `${path}.captureDigest`,
1187
+ ),
1188
+ );
1189
+ }
1190
+ }
1191
+ const hasDirectCapture =
1192
+ typeof source.capturePath === 'string' &&
1193
+ source.capturePath.trim().length > 0 &&
1194
+ isDigest(source.captureDigest);
1195
+ let hasValidValidatorState = false;
1196
+ let hasValidatorCapture = false;
1197
+ if (Object.hasOwn(source, 'validatorState')) {
1198
+ requiredObject(source, 'validatorState', errors, path);
1199
+ if (isObject(source.validatorState)) {
1200
+ const validatorPath = `${path}.validatorState`;
1201
+ closedObject(
1202
+ source.validatorState,
1203
+ new Set(['etag', 'lastModified', 'capturePath', 'captureDigest']),
1204
+ errors,
1205
+ validatorPath,
1206
+ );
1207
+ const validatorKeys = Object.keys(source.validatorState);
1208
+ const hasValidatorCaptureFields =
1209
+ Object.hasOwn(source.validatorState, 'capturePath') ||
1210
+ Object.hasOwn(source.validatorState, 'captureDigest');
1211
+ if (Object.hasOwn(source.validatorState, 'etag')) {
1212
+ requiredString(source.validatorState, 'etag', errors, validatorPath);
1213
+ }
1214
+ if (Object.hasOwn(source.validatorState, 'lastModified')) {
1215
+ requiredString(
1216
+ source.validatorState,
1217
+ 'lastModified',
1218
+ errors,
1219
+ validatorPath,
1220
+ );
1221
+ }
1222
+ if (hasValidatorCaptureFields) {
1223
+ requiredString(
1224
+ source.validatorState,
1225
+ 'capturePath',
1226
+ errors,
1227
+ validatorPath,
1228
+ );
1229
+ requiredString(
1230
+ source.validatorState,
1231
+ 'captureDigest',
1232
+ errors,
1233
+ validatorPath,
1234
+ );
1235
+ hasValidatorCapture =
1236
+ typeof source.validatorState.capturePath === 'string' &&
1237
+ source.validatorState.capturePath.trim().length > 0 &&
1238
+ isDigest(source.validatorState.captureDigest);
1239
+ if (!hasValidatorCapture) {
1240
+ errors.push(
1241
+ issue(
1242
+ 'INVALID_SOURCE_DESCRIPTOR',
1243
+ 'validatorState capturePath and captureDigest must form a complete sha256-bound pair',
1244
+ validatorPath,
1245
+ ),
1246
+ );
1247
+ }
1248
+ }
1249
+ hasValidValidatorState =
1250
+ (typeof source.validatorState.etag === 'string' &&
1251
+ source.validatorState.etag.trim().length > 0) ||
1252
+ (typeof source.validatorState.lastModified === 'string' &&
1253
+ source.validatorState.lastModified.trim().length > 0) ||
1254
+ hasValidatorCapture;
1255
+ if (validatorKeys.length === 0 || !hasValidValidatorState) {
1256
+ errors.push(
1257
+ issue(
1258
+ 'INVALID_SOURCE_DESCRIPTOR',
1259
+ 'validatorState must contain an etag, lastModified value, or complete capture pair',
1260
+ validatorPath,
1261
+ ),
1262
+ );
1263
+ }
1264
+ }
1265
+ }
1266
+ if (hasDirectCapture && hasValidatorCapture) {
1267
+ errors.push(
1268
+ issue(
1269
+ 'DUAL_URL_CAPTURE',
1270
+ 'URL sources cannot declare direct and validatorState capture pairs',
1271
+ path,
1272
+ ),
1273
+ );
1274
+ }
1275
+ if (!hasDirectCapture && !hasValidValidatorState) {
1276
+ errors.push(
1277
+ issue(
1278
+ 'MISSING_REQUIRED_FIELD',
1279
+ 'URL sources require a capture or validator state',
1280
+ path,
1281
+ ),
1282
+ );
1283
+ }
1284
+ } else if (source.kind === 'command-output') {
1285
+ requiredArray(source, 'argv', errors, path);
1286
+ requiredString(source, 'cwd', errors, path);
1287
+ requiredInteger(source, 'exitStatus', errors, path);
1288
+ requiredString(source, 'outputPath', errors, path);
1289
+ requiredString(source, 'outputDigest', errors, path);
1290
+ requiredArray(source, 'environmentNames', errors, path);
1291
+ if (!isDigest(source.outputDigest)) {
1292
+ errors.push(
1293
+ issue(
1294
+ 'INVALID_SOURCE_DESCRIPTOR',
1295
+ 'outputDigest must be a sha256 digest',
1296
+ `${path}.outputDigest`,
1297
+ ),
1298
+ );
1299
+ }
1300
+ } else {
1301
+ requiredString(source, 'system', errors, path);
1302
+ requiredString(source, 'resourceId', errors, path);
1303
+ const hasVersion =
1304
+ (typeof source.resourceVersion === 'string' &&
1305
+ source.resourceVersion.trim().length > 0) ||
1306
+ (typeof source.retrievalToken === 'string' &&
1307
+ source.retrievalToken.trim().length > 0);
1308
+ const hasCapture =
1309
+ Object.hasOwn(source, 'capturePath') ||
1310
+ Object.hasOwn(source, 'captureDigest');
1311
+ if (hasCapture) {
1312
+ requiredString(source, 'capturePath', errors, path);
1313
+ requiredString(source, 'captureDigest', errors, path);
1314
+ if (!isDigest(source.captureDigest)) {
1315
+ errors.push(
1316
+ issue(
1317
+ 'INVALID_SOURCE_DESCRIPTOR',
1318
+ 'captureDigest must be a sha256 digest',
1319
+ `${path}.captureDigest`,
1320
+ ),
1321
+ );
1322
+ }
1323
+ }
1324
+ if (!hasVersion && !hasCapture) {
1325
+ errors.push(
1326
+ issue(
1327
+ 'MISSING_REQUIRED_FIELD',
1328
+ 'Connected sources require a version, retrieval token, or capture',
1329
+ path,
1330
+ ),
1331
+ );
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ function validateReviewBriefArtifact(value, errors) {
1337
+ for (const key of ['id', 'runId', 'mode', 'createdAt']) {
1338
+ requiredString(value, key, errors);
1339
+ }
1340
+ requiredArray(value, 'excludedInputs', errors);
1341
+ if (value.mode === 'verify') {
1342
+ requiredArray(value, 'claims', errors);
1343
+ requiredArray(value, 'sources', errors);
1344
+ closedObject(
1345
+ value,
1346
+ new Set([
1347
+ 'kind',
1348
+ 'schemaVersion',
1349
+ 'id',
1350
+ 'runId',
1351
+ 'mode',
1352
+ 'createdAt',
1353
+ 'excludedInputs',
1354
+ 'claims',
1355
+ 'sources',
1356
+ ]),
1357
+ errors,
1358
+ );
1359
+ for (const [index, claim] of (value.claims ?? []).entries()) {
1360
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
1361
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
1362
+ requiredArray(claim, 'evidence', errors, `$.claims[${index}]`);
1363
+ closedObject(
1364
+ claim,
1365
+ new Set(['id', 'statement', 'evidence']),
1366
+ errors,
1367
+ `$.claims[${index}]`,
1368
+ );
1369
+ for (const [evidenceIndex, evidence] of (
1370
+ claim.evidence ?? []
1371
+ ).entries()) {
1372
+ for (const key of ['id', 'sourceId', 'displayExcerpt']) {
1373
+ requiredString(
1374
+ evidence,
1375
+ key,
1376
+ errors,
1377
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1378
+ );
1379
+ }
1380
+ requiredObject(
1381
+ evidence,
1382
+ 'locator',
1383
+ errors,
1384
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1385
+ );
1386
+ closedObject(
1387
+ evidence,
1388
+ new Set(['id', 'sourceId', 'displayExcerpt', 'locator']),
1389
+ errors,
1390
+ `$.claims[${index}].evidence[${evidenceIndex}]`,
1391
+ );
1392
+ }
1393
+ }
1394
+ for (const [index, source] of (value.sources ?? []).entries()) {
1395
+ validateReviewBriefSource(source, index, errors);
1396
+ }
1397
+ if (Array.isArray(value.sources)) {
1398
+ duplicateIds(value.sources, '$.sources', errors);
1399
+ const suppliedSourceIds = new Set(
1400
+ value.sources
1401
+ .map((source) => source?.id)
1402
+ .filter((id) => typeof id === 'string'),
1403
+ );
1404
+ const referencedSourceIds = new Set();
1405
+ for (const [claimIndex, claim] of (value.claims ?? []).entries()) {
1406
+ for (const [evidenceIndex, evidence] of (
1407
+ claim.evidence ?? []
1408
+ ).entries()) {
1409
+ if (typeof evidence?.sourceId !== 'string') continue;
1410
+ referencedSourceIds.add(evidence.sourceId);
1411
+ if (!suppliedSourceIds.has(evidence.sourceId)) {
1412
+ errors.push(
1413
+ issue(
1414
+ 'MISSING_SOURCE_DESCRIPTOR',
1415
+ `Evidence source ${evidence.sourceId} does not resolve to a verification brief source`,
1416
+ `$.claims[${claimIndex}].evidence[${evidenceIndex}].sourceId`,
1417
+ ),
1418
+ );
1419
+ }
1420
+ }
1421
+ }
1422
+ for (const [index, source] of value.sources.entries()) {
1423
+ if (
1424
+ typeof source?.id === 'string' &&
1425
+ !referencedSourceIds.has(source.id)
1426
+ ) {
1427
+ errors.push(
1428
+ issue(
1429
+ 'UNREFERENCED_SOURCE_DESCRIPTOR',
1430
+ `Verification brief source ${source.id} is not referenced by any claim evidence`,
1431
+ `$.sources[${index}].id`,
1432
+ ),
1433
+ );
1434
+ }
1435
+ }
1436
+ }
1437
+ } else if (value.mode === 'adversary') {
1438
+ requiredObject(value, 'scope', errors);
1439
+ requiredArray(value, 'questions', errors);
1440
+ requiredArray(value, 'provisionalStatements', errors);
1441
+ closedObject(
1442
+ value,
1443
+ new Set([
1444
+ 'kind',
1445
+ 'schemaVersion',
1446
+ 'id',
1447
+ 'runId',
1448
+ 'mode',
1449
+ 'createdAt',
1450
+ 'excludedInputs',
1451
+ 'scope',
1452
+ 'questions',
1453
+ 'provisionalStatements',
1454
+ ]),
1455
+ errors,
1456
+ );
1457
+ closedObject(
1458
+ value.scope,
1459
+ new Set(['included', 'excluded']),
1460
+ errors,
1461
+ '$.scope',
1462
+ );
1463
+ for (const key of ['included', 'excluded']) {
1464
+ requiredArray(value.scope, key, errors, '$.scope');
1465
+ }
1466
+ for (const [index, statement] of (
1467
+ value.provisionalStatements ?? []
1468
+ ).entries()) {
1469
+ requiredString(
1470
+ statement,
1471
+ 'id',
1472
+ errors,
1473
+ `$.provisionalStatements[${index}]`,
1474
+ );
1475
+ requiredString(
1476
+ statement,
1477
+ 'statement',
1478
+ errors,
1479
+ `$.provisionalStatements[${index}]`,
1480
+ );
1481
+ closedObject(
1482
+ statement,
1483
+ new Set(['id', 'statement']),
1484
+ errors,
1485
+ `$.provisionalStatements[${index}]`,
1486
+ );
1487
+ }
1488
+ } else if (value.mode === 'coverage') {
1489
+ requiredObject(value, 'scope', errors);
1490
+ requiredArray(value, 'questions', errors);
1491
+ requiredArray(value, 'claims', errors);
1492
+ closedObject(
1493
+ value,
1494
+ new Set([
1495
+ 'kind',
1496
+ 'schemaVersion',
1497
+ 'id',
1498
+ 'runId',
1499
+ 'mode',
1500
+ 'createdAt',
1501
+ 'excludedInputs',
1502
+ 'scope',
1503
+ 'questions',
1504
+ 'claims',
1505
+ ]),
1506
+ errors,
1507
+ );
1508
+ closedObject(
1509
+ value.scope,
1510
+ new Set(['included', 'excluded']),
1511
+ errors,
1512
+ '$.scope',
1513
+ );
1514
+ for (const key of ['included', 'excluded']) {
1515
+ requiredArray(value.scope, key, errors, '$.scope');
1516
+ }
1517
+ for (const [index, claim] of (value.claims ?? []).entries()) {
1518
+ requiredString(claim, 'id', errors, `$.claims[${index}]`);
1519
+ requiredString(claim, 'statement', errors, `$.claims[${index}]`);
1520
+ closedObject(
1521
+ claim,
1522
+ new Set(['id', 'statement']),
1523
+ errors,
1524
+ `$.claims[${index}]`,
1525
+ );
1526
+ }
1527
+ } else {
1528
+ errors.push(
1529
+ issue('INVALID_REVIEW_MODE', 'Unknown review brief mode', '$.mode'),
1530
+ );
1531
+ }
1532
+ }
1533
+
1534
+ function validateReviewResult(value, errors) {
1535
+ for (const key of ['id', 'runId', 'reviewKind', 'reviewerLane', 'status']) {
1536
+ requiredString(value, key, errors);
1537
+ }
1538
+ for (const key of [
1539
+ 'permittedInputs',
1540
+ 'excludedInputs',
1541
+ 'dispositions',
1542
+ 'newEvidence',
1543
+ 'coverageFindings',
1544
+ 'unresolvedIssues',
1545
+ ]) {
1546
+ requiredArray(value, key, errors);
1547
+ }
1548
+ if (
1549
+ ![
1550
+ 'semantic',
1551
+ 'adversarial',
1552
+ 'coverage',
1553
+ 'redundant-verification',
1554
+ 'contradiction-resolution',
1555
+ 'reconciliation',
1556
+ ].includes(value.reviewKind)
1557
+ ) {
1558
+ errors.push(
1559
+ issue(
1560
+ 'INVALID_REVIEW_KIND',
1561
+ 'Unknown review result kind',
1562
+ '$.reviewKind',
1563
+ ),
1564
+ );
1565
+ }
1566
+ if (!['complete', 'failed'].includes(value.status)) {
1567
+ errors.push(
1568
+ issue('INVALID_REVIEW_STATUS', 'Unknown review status', '$.status'),
1569
+ );
1570
+ }
1571
+ if (value.reviewKind === 'reconciliation') {
1572
+ requiredObject(value, 'inputLedger', errors);
1573
+ if (isObject(value.inputLedger)) {
1574
+ validateExactReference(value.inputLedger, '$.inputLedger', errors, [
1575
+ 'revision',
1576
+ ]);
1577
+ if (!Number.isInteger(value.inputLedger.revision)) {
1578
+ errors.push(
1579
+ issue(
1580
+ 'INVALID_REVISION',
1581
+ 'input ledger revision must be an integer',
1582
+ '$.inputLedger.revision',
1583
+ ),
1584
+ );
1585
+ }
1586
+ }
1587
+ if (!Number.isInteger(value.outputRevision)) {
1588
+ errors.push(
1589
+ issue(
1590
+ 'INVALID_REVISION',
1591
+ 'outputRevision must be an integer',
1592
+ '$.outputRevision',
1593
+ ),
1594
+ );
1595
+ }
1596
+ requiredArray(value, 'incorporatedReviewIds', errors);
1597
+ requiredArray(value, 'transitions', errors);
1598
+ requiredArray(value, 'additions', errors);
1599
+ requiredArray(value, 'removals', errors);
1600
+ requiredArray(value, 'removalDispositions', errors);
1601
+ requiredArray(value, 'coverageDispositions', errors);
1602
+ for (const key of ['additions', 'removals']) {
1603
+ const seen = new Set();
1604
+ for (const [index, id] of (value[key] ?? []).entries()) {
1605
+ if (typeof id !== 'string' || id.length === 0 || seen.has(id)) {
1606
+ errors.push(
1607
+ issue(
1608
+ 'INVALID_RECONCILIATION_MEMBERSHIP',
1609
+ `${key} must contain unique non-empty claim IDs`,
1610
+ `$.${key}[${index}]`,
1611
+ ),
1612
+ );
1613
+ }
1614
+ seen.add(id);
1615
+ }
1616
+ }
1617
+ for (const [index, disposition] of (
1618
+ value.removalDispositions ?? []
1619
+ ).entries()) {
1620
+ for (const key of ['claimId', 'reviewId', 'disposition']) {
1621
+ requiredString(
1622
+ disposition,
1623
+ key,
1624
+ errors,
1625
+ `$.removalDispositions[${index}]`,
1626
+ );
1627
+ }
1628
+ if (disposition.disposition !== 'rejected') {
1629
+ errors.push(
1630
+ issue(
1631
+ 'INVALID_REMOVAL_DISPOSITION',
1632
+ 'Claim removal must be authorized by a rejected review disposition',
1633
+ `$.removalDispositions[${index}].disposition`,
1634
+ ),
1635
+ );
1636
+ }
1637
+ closedObject(
1638
+ disposition,
1639
+ new Set(['claimId', 'reviewId', 'disposition']),
1640
+ errors,
1641
+ `$.removalDispositions[${index}]`,
1642
+ );
1643
+ }
1644
+ for (const [index, transition] of (value.transitions ?? []).entries()) {
1645
+ closedObject(
1646
+ transition,
1647
+ new Set(['claimId', 'from', 'to']),
1648
+ errors,
1649
+ `$.transitions[${index}]`,
1650
+ );
1651
+ requiredString(transition, 'claimId', errors, `$.transitions[${index}]`);
1652
+ if (!legalTransitions.has(`${transition.from}:${transition.to}`)) {
1653
+ errors.push(
1654
+ issue(
1655
+ 'ILLEGAL_CLAIM_TRANSITION',
1656
+ `Illegal claim transition ${transition.from} -> ${transition.to}`,
1657
+ `$.transitions[${index}]`,
1658
+ ),
1659
+ );
1660
+ }
1661
+ }
1662
+ for (const [index, disposition] of (
1663
+ value.coverageDispositions ?? []
1664
+ ).entries()) {
1665
+ for (const key of ['findingId', 'gapId', 'disposition']) {
1666
+ requiredString(
1667
+ disposition,
1668
+ key,
1669
+ errors,
1670
+ `$.coverageDispositions[${index}]`,
1671
+ );
1672
+ }
1673
+ if (!['accepted-gap', 'resolved'].includes(disposition.disposition)) {
1674
+ errors.push(
1675
+ issue(
1676
+ 'INVALID_COVERAGE_DISPOSITION',
1677
+ 'Coverage dispositions must be accepted-gap or resolved',
1678
+ `$.coverageDispositions[${index}].disposition`,
1679
+ ),
1680
+ );
1681
+ }
1682
+ if (
1683
+ disposition.disposition === 'resolved' &&
1684
+ (!Array.isArray(disposition.evidenceIds) ||
1685
+ disposition.evidenceIds.length === 0)
1686
+ ) {
1687
+ errors.push(
1688
+ issue(
1689
+ 'INVALID_COVERAGE_DISPOSITION',
1690
+ 'Resolved coverage dispositions require typed evidence IDs',
1691
+ `$.coverageDispositions[${index}].evidenceIds`,
1692
+ ),
1693
+ );
1694
+ }
1695
+ closedObject(
1696
+ disposition,
1697
+ new Set(['findingId', 'gapId', 'disposition', 'evidenceIds']),
1698
+ errors,
1699
+ `$.coverageDispositions[${index}]`,
1700
+ );
1701
+ }
1702
+ } else {
1703
+ validateExactReference(value.brief, '$.brief', errors);
1704
+ }
1705
+ for (const [index, reference] of (value.permittedInputs ?? []).entries()) {
1706
+ validateExactReference(reference, `$.permittedInputs[${index}]`, errors);
1707
+ }
1708
+ for (const [index, disposition] of (value.dispositions ?? []).entries()) {
1709
+ requiredString(disposition, 'claimId', errors, `$.dispositions[${index}]`);
1710
+ requiredString(
1711
+ disposition,
1712
+ 'disposition',
1713
+ errors,
1714
+ `$.dispositions[${index}]`,
1715
+ );
1716
+ closedObject(
1717
+ disposition,
1718
+ new Set(['claimId', 'disposition']),
1719
+ errors,
1720
+ `$.dispositions[${index}]`,
1721
+ );
1722
+ const allowed = {
1723
+ semantic: ['affirmed', 'rejected', 'uncertain'],
1724
+ adversarial: ['unchallenged', 'challenged'],
1725
+ coverage: ['covered', 'gap'],
1726
+ 'redundant-verification': ['affirmed', 'rejected', 'uncertain'],
1727
+ 'contradiction-resolution': ['resolved', 'unresolved', 'rejected'],
1728
+ reconciliation: [],
1729
+ }[value.reviewKind];
1730
+ if (allowed && !allowed.includes(disposition.disposition)) {
1731
+ errors.push(
1732
+ issue(
1733
+ 'INVALID_REVIEW_DISPOSITION',
1734
+ 'Disposition does not belong to this review kind',
1735
+ `$.dispositions[${index}].disposition`,
1736
+ ),
1737
+ );
1738
+ }
1739
+ }
1740
+ if (value.reviewKind === 'contradiction-resolution') {
1741
+ requiredArray(value, 'contradictionDispositions', errors);
1742
+ for (const [index, disposition] of (
1743
+ value.contradictionDispositions ?? []
1744
+ ).entries()) {
1745
+ requiredString(
1746
+ disposition,
1747
+ 'contradictionId',
1748
+ errors,
1749
+ `$.contradictionDispositions[${index}]`,
1750
+ );
1751
+ requiredArray(
1752
+ disposition,
1753
+ 'claimIds',
1754
+ errors,
1755
+ `$.contradictionDispositions[${index}]`,
1756
+ );
1757
+ requiredString(
1758
+ disposition,
1759
+ 'disposition',
1760
+ errors,
1761
+ `$.contradictionDispositions[${index}]`,
1762
+ );
1763
+ if (!['resolved', 'unresolved'].includes(disposition.disposition)) {
1764
+ errors.push(
1765
+ issue(
1766
+ 'INVALID_CONTRADICTION_DISPOSITION',
1767
+ 'Contradiction disposition must be resolved or unresolved',
1768
+ `$.contradictionDispositions[${index}].disposition`,
1769
+ ),
1770
+ );
1771
+ }
1772
+ closedObject(
1773
+ disposition,
1774
+ new Set(['contradictionId', 'claimIds', 'disposition']),
1775
+ errors,
1776
+ `$.contradictionDispositions[${index}]`,
1777
+ );
1778
+ }
1779
+ }
1780
+ if (
1781
+ ['redundant-verification', 'contradiction-resolution'].includes(
1782
+ value.reviewKind,
1783
+ ) &&
1784
+ ((value.dispositions ?? []).length === 0 ||
1785
+ (value.permittedInputs ?? []).length === 0 ||
1786
+ (value.reviewKind === 'contradiction-resolution' &&
1787
+ (value.contradictionDispositions ?? []).length === 0))
1788
+ ) {
1789
+ errors.push(
1790
+ issue(
1791
+ 'EMPTY_ASSURANCE_RESULT',
1792
+ 'Thorough assurance results must bind immutable inputs, claims, and affected contradictions',
1793
+ '$',
1794
+ ),
1795
+ );
1796
+ }
1797
+ for (const [index, finding] of (value.coverageFindings ?? []).entries()) {
1798
+ for (const key of ['id', 'gapId', 'code', 'message']) {
1799
+ requiredString(finding, key, errors, `$.coverageFindings[${index}]`);
1800
+ }
1801
+ requiredArray(finding, 'claimIds', errors, `$.coverageFindings[${index}]`);
1802
+ if (typeof finding.material !== 'boolean') {
1803
+ errors.push(
1804
+ issue(
1805
+ 'MISSING_REQUIRED_FIELD',
1806
+ 'material must be a boolean',
1807
+ `$.coverageFindings[${index}].material`,
1808
+ ),
1809
+ );
1810
+ }
1811
+ closedObject(
1812
+ finding,
1813
+ new Set(['id', 'gapId', 'code', 'message', 'material', 'claimIds']),
1814
+ errors,
1815
+ `$.coverageFindings[${index}]`,
1816
+ );
1817
+ }
1818
+ if (
1819
+ value.reviewKind !== 'coverage' &&
1820
+ value.reviewKind !== 'reconciliation' &&
1821
+ (value.coverageFindings ?? []).length > 0
1822
+ ) {
1823
+ errors.push(
1824
+ issue(
1825
+ 'INVALID_COVERAGE_FINDING_OWNER',
1826
+ 'Only coverage results may report coverage findings',
1827
+ '$.coverageFindings',
1828
+ ),
1829
+ );
1830
+ }
1831
+ for (const [index, evidence] of (value.newEvidence ?? []).entries()) {
1832
+ closedObject(
1833
+ evidence,
1834
+ new Set([
1835
+ 'id',
1836
+ 'sourceId',
1837
+ 'locator',
1838
+ 'displayExcerpt',
1839
+ 'observedAt',
1840
+ 'contentHash',
1841
+ 'locatorValidation',
1842
+ 'provenance',
1843
+ 'redaction',
1844
+ ]),
1845
+ errors,
1846
+ `$.newEvidence[${index}]`,
1847
+ );
1848
+ for (const key of ['id', 'sourceId', 'displayExcerpt', 'observedAt']) {
1849
+ requiredString(evidence, key, errors, `$.newEvidence[${index}]`);
1850
+ }
1851
+ requiredObject(evidence, 'locator', errors, `$.newEvidence[${index}]`);
1852
+ requiredObject(
1853
+ evidence,
1854
+ 'locatorValidation',
1855
+ errors,
1856
+ `$.newEvidence[${index}]`,
1857
+ );
1858
+ validateExactReference(
1859
+ evidence.provenance,
1860
+ `$.newEvidence[${index}].provenance`,
1861
+ errors,
1862
+ );
1863
+ }
1864
+ if (
1865
+ (value.newEvidence ?? []).length > 0 ||
1866
+ value.evidenceAssociations !== undefined
1867
+ ) {
1868
+ requiredArray(value, 'evidenceAssociations', errors);
1869
+ }
1870
+ for (const [index, association] of (
1871
+ value.evidenceAssociations ?? []
1872
+ ).entries()) {
1873
+ for (const key of ['evidenceId', 'claimId', 'relation']) {
1874
+ requiredString(
1875
+ association,
1876
+ key,
1877
+ errors,
1878
+ `$.evidenceAssociations[${index}]`,
1879
+ );
1880
+ }
1881
+ if (!evidenceRelations.includes(association.relation)) {
1882
+ errors.push(
1883
+ issue(
1884
+ 'INVALID_EVIDENCE_RELATION',
1885
+ 'Evidence association relation is not supported',
1886
+ `$.evidenceAssociations[${index}].relation`,
1887
+ ),
1888
+ );
1889
+ }
1890
+ closedObject(
1891
+ association,
1892
+ new Set(['evidenceId', 'claimId', 'relation']),
1893
+ errors,
1894
+ `$.evidenceAssociations[${index}]`,
1895
+ );
1896
+ }
1897
+ const common = [
1898
+ 'kind',
1899
+ 'schemaVersion',
1900
+ 'id',
1901
+ 'runId',
1902
+ 'reviewKind',
1903
+ 'reviewerLane',
1904
+ 'status',
1905
+ 'permittedInputs',
1906
+ 'excludedInputs',
1907
+ 'dispositions',
1908
+ 'newEvidence',
1909
+ 'evidenceAssociations',
1910
+ 'coverageFindings',
1911
+ 'unresolvedIssues',
1912
+ ];
1913
+ closedObject(
1914
+ value,
1915
+ new Set(
1916
+ value.reviewKind === 'reconciliation'
1917
+ ? [
1918
+ ...common,
1919
+ 'inputLedger',
1920
+ 'outputRevision',
1921
+ 'incorporatedReviewIds',
1922
+ 'transitions',
1923
+ 'additions',
1924
+ 'removals',
1925
+ 'removalDispositions',
1926
+ 'coverageDispositions',
1927
+ ]
1928
+ : value.reviewKind === 'contradiction-resolution'
1929
+ ? [...common, 'brief', 'contradictionDispositions']
1930
+ : [...common, 'brief'],
1931
+ ),
1932
+ errors,
1933
+ );
1934
+ }
1935
+
1936
+ export function validateArtifactShape(value) {
1937
+ const errors = [];
1938
+ if (!isObject(value)) {
1939
+ return {
1940
+ valid: false,
1941
+ errors: [issue('INVALID_ARTIFACT', 'Expected a JSON object')],
1942
+ };
1943
+ }
1944
+ if (!artifactKinds.has(value.kind)) {
1945
+ errors.push(
1946
+ issue(
1947
+ 'UNKNOWN_ARTIFACT_KIND',
1948
+ `Unknown artifact kind ${value.kind}`,
1949
+ '$.kind',
1950
+ ),
1951
+ );
1952
+ }
1953
+ if (value.schemaVersion !== SCHEMA_VERSION) {
1954
+ errors.push(
1955
+ issue(
1956
+ 'UNSUPPORTED_SCHEMA_VERSION',
1957
+ `Expected schemaVersion ${SCHEMA_VERSION}`,
1958
+ '$.schemaVersion',
1959
+ ),
1960
+ );
1961
+ }
1962
+ if (value.kind === 'recon.packet-manifest') validateManifest(value, errors);
1963
+ if (value.kind === 'recon.claim-ledger') validateLedger(value, errors);
1964
+ if (value.kind === 'recon.raw-dossier') validateDossier(value, errors);
1965
+ if (value.kind === 'recon.review-brief')
1966
+ validateReviewBriefArtifact(value, errors);
1967
+ if (value.kind === 'recon.review-result') validateReviewResult(value, errors);
1968
+ return { valid: errors.length === 0, errors };
1969
+ }