@open-agent-toolkit/cli 0.2.54 → 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.
@@ -6,8 +6,6 @@ export const artifactKinds = new Set([
6
6
  'recon.raw-dossier',
7
7
  'recon.review-brief',
8
8
  'recon.review-result',
9
- 'recon.stage-result',
10
- 'recon.dispatch-receipt',
11
9
  ]);
12
10
 
13
11
  export const profiles = ['quick', 'standard', 'thorough'];
@@ -36,11 +34,17 @@ export const workerModes = [
36
34
  'coverage',
37
35
  'reconcile',
38
36
  ];
39
- export const stageModes = [
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 = [
40
45
  'map',
41
46
  'gather',
42
47
  'compile',
43
- 'locator-validation',
44
48
  'semantic-verification',
45
49
  'adversarial',
46
50
  'coverage',
@@ -176,44 +180,6 @@ function requiredArray(value, key, errors, path = '$') {
176
180
  }
177
181
  }
178
182
 
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
183
  function requiredObject(value, key, errors, path = '$') {
218
184
  if (!isObject(value?.[key])) {
219
185
  errors.push(
@@ -254,401 +220,181 @@ function duplicateIds(values, path, errors) {
254
220
  }
255
221
  }
256
222
 
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([
223
+ const executionKeys = new Set([
299
224
  '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',
225
+ 'route',
226
+ 'role',
227
+ 'model',
228
+ 'effort',
229
+ 'reasoningMode',
230
+ 'serviceTier',
316
231
  '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',
232
+ 'maxConcurrency',
233
+ 'deadlineSeconds',
234
+ 'retryLimit',
235
+ 'waves',
236
+ 'approval',
334
237
  ]);
335
238
 
336
- export function validateApprovalProjection(value, errors, path) {
239
+ export function approvalFingerprintInput(execution) {
240
+ const { approval: _approval, ...approved } = execution;
241
+ return approved;
242
+ }
243
+
244
+ function validateApprovalEvidence(value, errors, path) {
337
245
  if (!isObject(value)) return;
338
- closedObject(value, projectionKeys, errors, path);
339
- if (value.schema !== 'oat-dispatch-approval/v1') {
246
+ closedObject(
247
+ value,
248
+ new Set(['type', 'approvedAt', 'fingerprint']),
249
+ errors,
250
+ path,
251
+ );
252
+ if (value.type !== 'explicit-user-approval') {
340
253
  errors.push(
341
254
  issue(
342
- 'INVALID_APPROVAL_SCHEMA',
343
- 'Approval projection must use oat-dispatch-approval/v1',
344
- `${path}.schema`,
255
+ 'INVALID_APPROVAL_EVIDENCE',
256
+ 'Approval evidence must record explicit user approval',
257
+ `${path}.type`,
345
258
  ),
346
259
  );
347
260
  }
348
- if (value.prepared_record_version !== 1) {
261
+ requiredTimestamp(value, 'approvedAt', errors, path);
262
+ if (!isDigest(value.fingerprint)) {
349
263
  errors.push(
350
264
  issue(
351
- 'INVALID_PREPARED_RECORD_VERSION',
352
- 'Prepared record version must be 1',
353
- `${path}.prepared_record_version`,
265
+ 'INVALID_APPROVAL_FINGERPRINT',
266
+ 'Approval evidence fingerprint must be sha256',
267
+ `${path}.fingerprint`,
354
268
  ),
355
269
  );
356
270
  }
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);
271
+ }
363
272
 
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`);
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);
374
278
  }
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;
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
+ );
391
288
  }
392
- requiredString(value.selection, key, errors, `${path}.selection`);
393
289
  }
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`);
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`);
415
305
  const waveIds = new Set();
416
306
  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
- }
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);
431
316
  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') {
317
+ if (Array.isArray(wave?.lanes) && wave.lanes.length === 0) {
440
318
  errors.push(
441
319
  issue(
442
320
  'MISSING_REQUIRED_FIELD',
443
- 'conditional must be a boolean',
444
- `${wavePath}.conditional`,
321
+ 'Every approved wave needs at least one lane',
322
+ `${wavePath}.lanes`,
445
323
  ),
446
324
  );
447
325
  }
448
- if (!taskClasses.includes(wave.task_class)) {
326
+ if (!waveModes.includes(wave?.mode)) {
449
327
  errors.push(
450
- issue(
451
- 'INVALID_TASK_CLASS',
452
- 'Unknown wave task class',
453
- `${wavePath}.task_class`,
454
- ),
328
+ issue('INVALID_WAVE_MODE', 'Unknown wave mode', `${wavePath}.mode`),
455
329
  );
456
330
  }
457
- if (wave.model_class_floor !== wave.task_class) {
331
+ if (!taskClasses.includes(wave?.taskClass)) {
458
332
  errors.push(
459
333
  issue(
460
- 'MODEL_CLASS_FLOOR_MISMATCH',
461
- 'Wave model class floor must equal its task class',
462
- `${wavePath}.model_class_floor`,
334
+ 'INVALID_TASK_CLASS',
335
+ 'Unknown wave task class',
336
+ `${wavePath}.taskClass`,
463
337
  ),
464
338
  );
465
339
  }
466
- if (!isDigest(wave.payload_digest)) {
340
+ if (typeof wave?.conditional !== 'boolean') {
467
341
  errors.push(
468
342
  issue(
469
- 'INVALID_PAYLOAD_DIGEST',
470
- 'Wave payload digest must be sha256',
471
- `${wavePath}.payload_digest`,
343
+ 'MISSING_REQUIRED_FIELD',
344
+ 'conditional must be a boolean',
345
+ `${wavePath}.conditional`,
472
346
  ),
473
347
  );
474
348
  }
475
- if (waveIds.has(wave.wave_id)) {
349
+ if (waveIds.has(wave?.waveId)) {
476
350
  errors.push(
477
351
  issue(
478
352
  'DUPLICATE_ID',
479
- `Duplicate wave ${wave.wave_id}`,
480
- `${wavePath}.wave_id`,
353
+ `Duplicate wave ${wave.waveId}`,
354
+ `${wavePath}.waveId`,
481
355
  ),
482
356
  );
483
357
  }
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()) {
358
+ waveIds.add(wave?.waveId);
359
+ for (const [laneIndex, lane] of (Array.isArray(wave?.lanes)
360
+ ? wave.lanes
361
+ : []
362
+ ).entries()) {
505
363
  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)) {
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)) {
510
387
  errors.push(
511
388
  issue(
512
389
  'DUPLICATE_ID',
513
- `Duplicate lane ${lane.lane_id}`,
514
- `${lanePath}.lane_id`,
390
+ `Duplicate lane ${lane.laneId}`,
391
+ `${lanePath}.laneId`,
515
392
  ),
516
393
  );
517
394
  }
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
- );
395
+ laneIds.add(lane?.laneId);
573
396
  }
574
397
  }
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
398
  }
653
399
 
654
400
  function validateManifest(value, errors) {
@@ -661,7 +407,6 @@ function validateManifest(value, errors) {
661
407
  'request',
662
408
  'sources',
663
409
  'execution',
664
- 'stages',
665
410
  'artifacts',
666
411
  'gaps',
667
412
  ]),
@@ -725,7 +470,6 @@ function validateManifest(value, errors) {
725
470
  }
726
471
  }
727
472
  requiredArray(value, 'sources', errors);
728
- requiredArray(value, 'stages', errors);
729
473
  requiredArray(value, 'artifacts', errors);
730
474
  requiredArray(value, 'gaps', errors);
731
475
  requiredObject(value, 'request', errors);
@@ -755,61 +499,9 @@ function validateManifest(value, errors) {
755
499
  requiredArray(value.request, key, errors, '$.request');
756
500
  }
757
501
  }
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
- }
502
+ validateExecution(value.execution, errors, '$.execution');
809
503
  if (Array.isArray(value.sources))
810
504
  duplicateIds(value.sources, '$.sources', errors);
811
- if (Array.isArray(value.stages))
812
- duplicateIds(value.stages, '$.stages', errors);
813
505
  if (Array.isArray(value.gaps)) duplicateIds(value.gaps, '$.gaps', errors);
814
506
  for (const [index, reference] of (value.artifacts ?? []).entries()) {
815
507
  validateExactReference(reference, `$.artifacts[${index}]`, errors);
@@ -951,62 +643,6 @@ function validateManifest(value, errors) {
951
643
  `$.gaps[${index}]`,
952
644
  );
953
645
  }
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
646
  }
1011
647
 
1012
648
  function validateLedger(value, errors) {
@@ -1116,6 +752,11 @@ function validateLedger(value, errors) {
1116
752
  for (const key of ['evidence', 'claims', 'unresolvedQuestions']) {
1117
753
  if (Array.isArray(value[key])) duplicateIds(value[key], `$.${key}`, errors);
1118
754
  }
755
+ const evidenceIds = new Set(
756
+ Array.isArray(value.evidence)
757
+ ? value.evidence.map((item) => item?.id).filter(Boolean)
758
+ : [],
759
+ );
1119
760
  for (const [index, reference] of (value.inputArtifacts ?? []).entries()) {
1120
761
  validateExactReference(reference, `$.inputArtifacts[${index}]`, errors);
1121
762
  }
@@ -1252,6 +893,53 @@ function validateLedger(value, errors) {
1252
893
  ]) {
1253
894
  requiredArray(claim, key, errors, `$.claims[${index}]`);
1254
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
+ }
1255
943
  for (const [referenceIndex, reference] of (
1256
944
  claim.derivedFrom ?? []
1257
945
  ).entries()) {
@@ -2190,11 +1878,7 @@ function validateReviewResult(value, errors) {
2190
1878
  `$.evidenceAssociations[${index}]`,
2191
1879
  );
2192
1880
  }
2193
- if (
2194
- !['supports', 'contradicts', 'qualifies', 'context'].includes(
2195
- association.relation,
2196
- )
2197
- ) {
1881
+ if (!evidenceRelations.includes(association.relation)) {
2198
1882
  errors.push(
2199
1883
  issue(
2200
1884
  'INVALID_EVIDENCE_RELATION',
@@ -2249,180 +1933,6 @@ function validateReviewResult(value, errors) {
2249
1933
  );
2250
1934
  }
2251
1935
 
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
1936
  export function validateArtifactShape(value) {
2427
1937
  const errors = [];
2428
1938
  if (!isObject(value)) {
@@ -2455,8 +1965,5 @@ export function validateArtifactShape(value) {
2455
1965
  if (value.kind === 'recon.review-brief')
2456
1966
  validateReviewBriefArtifact(value, errors);
2457
1967
  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
1968
  return { valid: errors.length === 0, errors };
2462
1969
  }