@open-agent-toolkit/cli 0.2.26 → 0.2.28

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 (76) hide show
  1. package/assets/NOTICES.md +156 -0
  2. package/assets/agents/oat-phase-implementer.md +312 -7
  3. package/assets/docs/contributing/explainer-kit-verification.md +125 -0
  4. package/assets/docs/contributing/index.md +1 -0
  5. package/assets/docs/reference/troubleshooting.md +47 -0
  6. package/assets/docs/workflows/projects/artifacts.md +24 -6
  7. package/assets/docs/workflows/projects/implementation-execution.md +151 -1
  8. package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
  9. package/assets/docs/workflows/skills/explainer-kit.md +121 -69
  10. package/assets/docs/workflows/skills/index.md +1 -0
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/explainer-kit/SKILL.md +18 -3
  13. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
  14. package/assets/skills/explainer-kit/references/contracts.md +167 -20
  15. package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
  16. package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
  17. package/assets/skills/explainer-kit/references/visual-review.md +57 -0
  18. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
  19. package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
  20. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
  21. package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
  22. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
  23. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
  24. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
  25. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
  26. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
  27. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
  28. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
  29. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
  30. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
  31. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
  32. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
  33. package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
  34. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
  35. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
  36. package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
  37. package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
  38. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
  39. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
  40. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
  41. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
  42. package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
  43. package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
  44. package/assets/skills/oat-dispatch-subagents/SKILL.md +16 -3
  45. package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
  46. package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
  47. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
  48. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
  49. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
  50. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
  51. package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
  52. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  53. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  54. package/assets/skills/oat-project-implement/SKILL.md +5 -4
  55. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +21 -0
  56. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  57. package/assets/skills/oat-project-implement/references/phase-execution.md +359 -12
  58. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  59. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  60. package/assets/templates/state.md +7 -0
  61. package/dist/commands/project/archive/archive-utils.d.ts +1 -0
  62. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  63. package/dist/commands/project/archive/archive-utils.js +109 -42
  64. package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
  65. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
  66. package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
  67. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
  68. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
  69. package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
  70. package/dist/commands/project/archive/push-runner.d.ts +2 -1
  71. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  72. package/dist/commands/project/archive/push-runner.js +5 -1
  73. package/dist/release/public-package-contract.d.ts +6 -0
  74. package/dist/release/public-package-contract.d.ts.map +1 -1
  75. package/dist/release/public-package-contract.js +75 -0
  76. package/package.json +2 -2
@@ -2,6 +2,10 @@ import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
 
4
4
  import { validatePortablePath } from './safe-paths.mjs';
5
+ import {
6
+ parseCanonicalGithubBlobUrl,
7
+ validateCanonicalGithubBlobTuple,
8
+ } from './source-backlinks.mjs';
5
9
 
6
10
  const SCHEMA_FILES = {
7
11
  'run-request': 'run-request.schema.json',
@@ -14,6 +18,9 @@ const SCHEMA_FILES = {
14
18
  'publish-receipt': 'publish-receipt.schema.json',
15
19
  'author-request/v2': 'author-request.v2.schema.json',
16
20
  'author-result/v2': 'author-result.v2.schema.json',
21
+ 'set-plan': 'set-plan.v1.schema.json',
22
+ 'visual-review-request': 'visual-review-request.v1.schema.json',
23
+ 'visual-review-result': 'visual-review-result.v1.schema.json',
17
24
  };
18
25
  const DEFAULT_SCHEMA_KEYS = {
19
26
  'author-request': 'author-request/v2',
@@ -47,6 +54,13 @@ const RAW_SECRET_KEYS = new Set([
47
54
  ]);
48
55
  const DATE_TIME_PATTERN =
49
56
  /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
57
+ const SET_PLAN_RECORD_PATHS = [
58
+ 'source/set-plan/request.json',
59
+ 'source/set-plan/result.json',
60
+ 'source/set-plan/ledger.json',
61
+ 'source/set-plan/portfolio.json',
62
+ 'source/set-plan/drafts.json',
63
+ ];
50
64
 
51
65
  export function validateContract(kind, value, context = {}) {
52
66
  const schema = resolveContractSchema(kind, value);
@@ -68,9 +82,51 @@ export function validateContract(kind, value, context = {}) {
68
82
  validateSchema(schema, value, '$', schema, errors);
69
83
  validateContractPaths(kind, value, errors);
70
84
  validateCrossRecord(kind, value, context, errors);
85
+ validateSourceBacklinks(kind, value, errors);
71
86
  return { valid: errors.length === 0, errors };
72
87
  }
73
88
 
89
+ function validateSourceBacklinks(kind, value, errors) {
90
+ if (kind === 'fact-base' || kind === 'explainer-kit.fact-base/v1') {
91
+ const tuples = [
92
+ ...(value.sources ?? []),
93
+ ...(value.claims ?? []).flatMap((claim) => claim.citations ?? []),
94
+ ...(value.unresolvedClaims ?? []).flatMap(
95
+ (claim) => claim.citations ?? [],
96
+ ),
97
+ ];
98
+ tuples.forEach((tuple, index) => {
99
+ const declaresBacklink = ['repository', 'path', 'lineRange', 'url'].some(
100
+ (field) => tuple[field] !== undefined,
101
+ );
102
+ if (declaresBacklink && !validateCanonicalGithubBlobTuple(tuple)) {
103
+ add(
104
+ errors,
105
+ `$.sourceBacklinks[${index}]`,
106
+ 'source-backlink',
107
+ 'Must declare one complete canonical GitHub blob backlink tuple.',
108
+ );
109
+ }
110
+ });
111
+ return;
112
+ }
113
+ if (kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1') return;
114
+ (value.source?.backlinks ?? []).forEach((entry, index) => {
115
+ try {
116
+ parseCanonicalGithubBlobUrl(entry.url);
117
+ } catch (error) {
118
+ add(
119
+ errors,
120
+ `$.source.backlinks[${index}].url`,
121
+ 'source-backlink',
122
+ error instanceof Error
123
+ ? error.message
124
+ : 'Must be a canonical GitHub blob backlink.',
125
+ );
126
+ }
127
+ });
128
+ }
129
+
74
130
  function resolveContractSchema(kind, value) {
75
131
  if (SCHEMAS[kind]) {
76
132
  return SCHEMAS[kind];
@@ -475,6 +531,260 @@ function validateCrossRecord(kind, value, context, errors) {
475
531
  'Retaining raw art direction requires theme.artDirection.',
476
532
  );
477
533
  }
534
+
535
+ if (value.recapMode !== undefined && value.recipe?.id !== 'project-recap') {
536
+ add(
537
+ errors,
538
+ '$.recapMode',
539
+ 'recap-mode-recipe',
540
+ 'recapMode is allowed only for the project-recap recipe.',
541
+ );
542
+ }
543
+ }
544
+
545
+ if (kind === 'set-plan') {
546
+ validateSetPlan(value, errors);
547
+ }
548
+
549
+ if (
550
+ ['author-request', 'author-request/v2'].includes(kind) ||
551
+ value.schemaVersion === 'explainer-kit.author-request/v2'
552
+ ) {
553
+ validateAuthorSetContext(value, errors);
554
+ validateVisualAuthoringGuidance(value, errors);
555
+ validateAuthorGraphSemantics(value, errors);
556
+ }
557
+
558
+ if (kind === 'visual-review-request') {
559
+ if (
560
+ typeof value.requestHash === 'string' &&
561
+ value.requestHash !== canonicalHash(visualReviewRequestPayload(value))
562
+ ) {
563
+ add(
564
+ errors,
565
+ '$.requestHash',
566
+ 'request-hash-mismatch',
567
+ 'Visual review request hash does not match its canonical evidence payload.',
568
+ );
569
+ }
570
+ if (
571
+ typeof value.requestHash === 'string' &&
572
+ typeof value.requestId === 'string' &&
573
+ value.requestId !== visualReviewRequestId(value.requestHash)
574
+ ) {
575
+ add(
576
+ errors,
577
+ '$.requestId',
578
+ 'request-id-mismatch',
579
+ 'Visual review request identity does not match its canonical request hash.',
580
+ );
581
+ }
582
+ const plannedArtifactIds = Array.isArray(value.plan?.portfolio)
583
+ ? value.plan.portfolio.map(({ artifactId }) => artifactId)
584
+ : [];
585
+ const plannedIds = new Set(plannedArtifactIds);
586
+ const renderedIds = new Set();
587
+ const requiresObservedCohesion = value.plan?.recipe?.id === 'project-recap';
588
+ const expectedCohesion = expectedLedgerClaims(value.plan?.ledger);
589
+ const observedCohesion = new Set();
590
+ if (
591
+ requiresObservedCohesion &&
592
+ ['terminology', 'statuses', 'numericClaims'].some(
593
+ (group) => expectedCohesion[group].size === 0,
594
+ )
595
+ ) {
596
+ add(
597
+ errors,
598
+ '$.plan.ledger',
599
+ 'cohesion-ledger-empty',
600
+ 'Adaptive recap review requires non-empty terminology, status, and numeric ledger entries.',
601
+ );
602
+ }
603
+ for (const [index, artifact] of (Array.isArray(value.renderedArtifacts)
604
+ ? value.renderedArtifacts
605
+ : []
606
+ ).entries()) {
607
+ for (const [evidenceIndex, evidence] of (Array.isArray(artifact?.evidence)
608
+ ? artifact.evidence
609
+ : []
610
+ ).entries()) {
611
+ if (evidence?.captureIdentity !== value.captureIdentity) {
612
+ add(
613
+ errors,
614
+ `$.renderedArtifacts[${index}].evidence[${evidenceIndex}].captureIdentity`,
615
+ 'browser-runtime-mismatch',
616
+ 'Every visual-review evidence record must bind the request browser capture identity.',
617
+ );
618
+ }
619
+ }
620
+ if (!plannedIds.has(artifact?.artifactId)) {
621
+ add(
622
+ errors,
623
+ `$.renderedArtifacts[${index}].artifactId`,
624
+ 'unknown-artifact',
625
+ 'Rendered artifact is not present in the shared set plan.',
626
+ );
627
+ }
628
+ if (renderedIds.has(artifact?.artifactId)) {
629
+ add(
630
+ errors,
631
+ `$.renderedArtifacts[${index}].artifactId`,
632
+ 'duplicate-artifact',
633
+ 'Rendered artifact IDs must be unique.',
634
+ );
635
+ }
636
+ renderedIds.add(artifact?.artifactId);
637
+ const observations = Array.isArray(artifact?.cohesionObservations)
638
+ ? artifact.cohesionObservations
639
+ : [];
640
+ if (requiresObservedCohesion && observations.length === 0) {
641
+ add(
642
+ errors,
643
+ `$.renderedArtifacts[${index}].cohesionObservations`,
644
+ 'cohesion-observations-empty',
645
+ 'Every adaptive recap artifact must expose observed shared-ledger evidence.',
646
+ );
647
+ }
648
+ for (const [observationIndex, observation] of observations.entries()) {
649
+ if (
650
+ observation?.artifactId !== artifact?.artifactId ||
651
+ observation?.contentHash !== artifact?.renderedHash
652
+ ) {
653
+ add(
654
+ errors,
655
+ `$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
656
+ 'cohesion-binding-mismatch',
657
+ 'Cohesion observations must bind to their artifact and exact rendered content hash.',
658
+ );
659
+ continue;
660
+ }
661
+ const expected = expectedCohesion[observation.group]?.get(
662
+ observation.claim,
663
+ );
664
+ if (
665
+ expected === undefined ||
666
+ normalizeComparable(expected) !==
667
+ normalizeComparable(observation.value)
668
+ ) {
669
+ add(
670
+ errors,
671
+ `$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
672
+ 'cohesion-contradiction',
673
+ 'Observed cohesion evidence must match an applicable shared-ledger value.',
674
+ );
675
+ continue;
676
+ }
677
+ observedCohesion.add(`${observation.group}:${observation.claim}`);
678
+ }
679
+ }
680
+ for (const artifactId of plannedArtifactIds) {
681
+ if (!renderedIds.has(artifactId)) {
682
+ add(
683
+ errors,
684
+ '$.renderedArtifacts',
685
+ 'missing-artifact',
686
+ `Rendered review set is missing planned artifact ${artifactId}.`,
687
+ );
688
+ }
689
+ }
690
+ if (requiresObservedCohesion) {
691
+ for (const [group, claims] of Object.entries(expectedCohesion)) {
692
+ for (const claim of claims.keys()) {
693
+ if (!observedCohesion.has(`${group}:${claim}`)) {
694
+ add(
695
+ errors,
696
+ '$.renderedArtifacts',
697
+ 'cohesion-claim-unobserved',
698
+ `Shared-ledger claim ${group}.${claim} is not observable in the rendered set.`,
699
+ );
700
+ }
701
+ }
702
+ }
703
+ }
704
+ }
705
+
706
+ if (kind === 'visual-review-result') {
707
+ const reviewRequest = context.visualReviewRequest;
708
+ if (!isObject(reviewRequest)) {
709
+ add(
710
+ errors,
711
+ '$',
712
+ 'review-request-required',
713
+ 'Visual review results must be validated with their reviewed request.',
714
+ );
715
+ return;
716
+ }
717
+
718
+ const requestValidation = validateContract(
719
+ 'visual-review-request',
720
+ reviewRequest,
721
+ );
722
+ if (!requestValidation.valid) {
723
+ add(
724
+ errors,
725
+ '$',
726
+ 'invalid-review-request',
727
+ 'Visual review result cannot bind to an invalid reviewed request.',
728
+ );
729
+ }
730
+ if (
731
+ value.requestId !== reviewRequest.requestId ||
732
+ value.requestHash !== reviewRequest.requestHash
733
+ ) {
734
+ add(
735
+ errors,
736
+ '$.requestHash',
737
+ 'review-binding-mismatch',
738
+ 'Visual review result must echo the exact reviewed request identity and hash.',
739
+ );
740
+ }
741
+
742
+ const reviewedArtifactIds = Array.isArray(reviewRequest.renderedArtifacts)
743
+ ? reviewRequest.renderedArtifacts.map(({ artifactId }) => artifactId)
744
+ : [];
745
+ const reviewedIds = new Set(reviewedArtifactIds);
746
+ const resultArtifactIds = Array.isArray(value.artifactIds)
747
+ ? value.artifactIds
748
+ : [];
749
+ const resultIds = new Set(resultArtifactIds);
750
+ if (
751
+ resultIds.size !== reviewedIds.size ||
752
+ reviewedArtifactIds.some((artifactId) => !resultIds.has(artifactId))
753
+ ) {
754
+ add(
755
+ errors,
756
+ '$.artifactIds',
757
+ 'review-set-mismatch',
758
+ 'Visual review result artifact IDs must equal the complete reviewed request set.',
759
+ );
760
+ }
761
+ for (const [index, finding] of (Array.isArray(value.findings)
762
+ ? value.findings
763
+ : []
764
+ ).entries()) {
765
+ if (!reviewedIds.has(finding?.artifactId)) {
766
+ add(
767
+ errors,
768
+ `$.findings[${index}].artifactId`,
769
+ 'detached-finding',
770
+ 'Visual review findings must reference an artifact in the reviewed request.',
771
+ );
772
+ }
773
+ }
774
+ const findingCount = Array.isArray(value.findings)
775
+ ? value.findings.length
776
+ : 0;
777
+ if (
778
+ (value.disposition === 'pass' && findingCount > 0) ||
779
+ (['correct', 'fail'].includes(value.disposition) && findingCount === 0)
780
+ ) {
781
+ add(
782
+ errors,
783
+ '$.disposition',
784
+ 'disposition-findings-mismatch',
785
+ 'Pass requires no findings; correct and fail require at least one correction finding.',
786
+ );
787
+ }
478
788
  }
479
789
 
480
790
  if (kind === 'manifest') {
@@ -533,10 +843,17 @@ function validateCrossRecord(kind, value, context, errors) {
533
843
  'run-request.json',
534
844
  'source/content-approval.json',
535
845
  ];
846
+ const recordedImmutable = isObject(value.immutableHashes)
847
+ ? new Set(Object.keys(value.immutableHashes))
848
+ : new Set();
849
+ const retainsSetPlan =
850
+ value.recipe?.id === 'project-recap' ||
851
+ SET_PLAN_RECORD_PATHS.some((path) => recordedImmutable.has(path));
536
852
  const expectedImmutable = new Set([
537
853
  ...requiredProvenance,
538
854
  value.source?.factBasePath,
539
855
  'source/fact-base.md',
856
+ ...(retainsSetPlan ? SET_PLAN_RECORD_PATHS : []),
540
857
  ...(Array.isArray(value.source?.authorResultPaths)
541
858
  ? value.source.authorResultPaths
542
859
  : []),
@@ -552,9 +869,6 @@ function validateCrossRecord(kind, value, context, errors) {
552
869
  : []),
553
870
  ]);
554
871
  expectedImmutable.delete(undefined);
555
- const recordedImmutable = isObject(value.immutableHashes)
556
- ? new Set(Object.keys(value.immutableHashes))
557
- : new Set();
558
872
  const missingLegacyPaths = requiredProvenance.filter(
559
873
  (path) => !recordedImmutable.has(path),
560
874
  );
@@ -566,17 +880,46 @@ function validateCrossRecord(kind, value, context, errors) {
566
880
  `Legacy manifest is missing immutable coverage for ${missingLegacyPaths.join(', ')}; regenerate the recap package before archival.`,
567
881
  );
568
882
  }
883
+ if ([...expectedImmutable].some((path) => !recordedImmutable.has(path))) {
884
+ add(
885
+ errors,
886
+ '$.immutableHashes',
887
+ 'immutable-package-incomplete',
888
+ 'Manifest immutable hashes must cover the complete retained fact-base, set plan, content, theme, and required built artifact package.',
889
+ );
890
+ }
891
+ const hasVisualEvidence = [...recordedImmutable].some((path) =>
892
+ path.startsWith('qa/visual-review/'),
893
+ );
569
894
  if (
570
- expectedImmutable.size !== recordedImmutable.size ||
571
- [...expectedImmutable].some((path) => !recordedImmutable.has(path))
895
+ hasVisualEvidence &&
896
+ [
897
+ 'qa/visual-review/attempt-1/request.json',
898
+ 'qa/visual-review/attempt-1/result.json',
899
+ ].some((path) => !recordedImmutable.has(path))
572
900
  ) {
573
901
  add(
574
902
  errors,
575
903
  '$.immutableHashes',
576
- 'immutable-package-incomplete',
577
- 'Manifest immutable hashes must cover the complete retained fact-base, content, theme, and required built artifact package.',
904
+ 'visual-review-chain-incomplete',
905
+ 'Retained visual review evidence requires its bound attempt request and result.',
578
906
  );
579
907
  }
908
+ for (const path of recordedImmutable) {
909
+ if (
910
+ (path.startsWith('qa/browser/') ||
911
+ path.includes('/visual-review/attempt-')) &&
912
+ path.endsWith('.png') &&
913
+ !recordedImmutable.has(path.replace(/\.png$/, '.json'))
914
+ ) {
915
+ add(
916
+ errors,
917
+ '$.immutableHashes',
918
+ 'visual-review-chain-incomplete',
919
+ `Screenshot evidence ${path} is missing its immutable metrics record.`,
920
+ );
921
+ }
922
+ }
580
923
 
581
924
  const record = context.buildRecord;
582
925
  if (isObject(record)) {
@@ -636,10 +979,24 @@ function validateCrossRecord(kind, value, context, errors) {
636
979
  ) {
637
980
  const expected = new Map();
638
981
  for (const artifact of context.manifest.artifacts ?? []) {
639
- if (isObject(artifact) && typeof artifact.renderedPath === 'string') {
982
+ if (
983
+ isObject(artifact) &&
984
+ artifact.status === 'built' &&
985
+ typeof artifact.renderedPath === 'string'
986
+ ) {
640
987
  expected.set(artifact.renderedPath, artifact.hash);
641
988
  }
642
989
  }
990
+ if (
991
+ isObject(context.catalogArtifact) &&
992
+ typeof context.catalogArtifact.relativePath === 'string'
993
+ ) {
994
+ expected.set(
995
+ context.catalogArtifact.relativePath,
996
+ context.catalogArtifact.hash,
997
+ );
998
+ }
999
+ const received = new Set();
643
1000
  for (const artifact of value.artifacts) {
644
1001
  if (
645
1002
  isObject(artifact) &&
@@ -652,6 +1009,227 @@ function validateCrossRecord(kind, value, context, errors) {
652
1009
  'Publish receipt artifact does not match the manifest.',
653
1010
  );
654
1011
  }
1012
+ if (received.has(artifact?.relativePath)) {
1013
+ add(
1014
+ errors,
1015
+ '$.artifacts',
1016
+ 'receipt-artifact-parity',
1017
+ 'Publish receipt artifact paths must be unique.',
1018
+ );
1019
+ }
1020
+ received.add(artifact?.relativePath);
1021
+ }
1022
+ if (
1023
+ received.size !== expected.size ||
1024
+ [...expected.keys()].some((path) => !received.has(path))
1025
+ ) {
1026
+ add(
1027
+ errors,
1028
+ '$.artifacts',
1029
+ 'receipt-artifact-parity',
1030
+ 'Publish receipt must exactly cover every manifest artifact and the generated catalog.',
1031
+ );
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+ export function visualReviewRequestPayload(request) {
1037
+ const {
1038
+ requestId: _requestId,
1039
+ requestHash: _requestHash,
1040
+ ...payload
1041
+ } = request;
1042
+ return payload;
1043
+ }
1044
+
1045
+ export function visualReviewRequestId(requestHash) {
1046
+ return `visual-review-${String(requestHash).replace(/^sha256:/, '')}`;
1047
+ }
1048
+
1049
+ function expectedLedgerClaims(ledger) {
1050
+ return {
1051
+ terminology: new Map(
1052
+ (ledger?.terminology ?? []).map(({ term }) => [term, term]),
1053
+ ),
1054
+ statuses: new Map(
1055
+ (ledger?.statuses ?? []).map(({ subject, value }) => [subject, value]),
1056
+ ),
1057
+ numericClaims: new Map(
1058
+ (ledger?.numbers ?? []).map(({ subject, value }) => [subject, value]),
1059
+ ),
1060
+ };
1061
+ }
1062
+
1063
+ function normalizeComparable(value) {
1064
+ return String(value).trim().toLocaleLowerCase();
1065
+ }
1066
+
1067
+ function validateSetPlan(value, errors) {
1068
+ const sourceIds = new Set(
1069
+ Array.isArray(value.sourceIds) ? value.sourceIds : [],
1070
+ );
1071
+ const artifactIds = new Set();
1072
+ for (const [index, artifact] of (Array.isArray(value.portfolio)
1073
+ ? value.portfolio
1074
+ : []
1075
+ ).entries()) {
1076
+ if (artifactIds.has(artifact?.artifactId)) {
1077
+ add(
1078
+ errors,
1079
+ `$.portfolio[${index}].artifactId`,
1080
+ 'duplicate-artifact',
1081
+ 'Set-plan artifact IDs must be unique.',
1082
+ );
1083
+ }
1084
+ artifactIds.add(artifact?.artifactId);
1085
+ for (const sourceId of Array.isArray(artifact?.sourceIds)
1086
+ ? artifact.sourceIds
1087
+ : []) {
1088
+ if (!sourceIds.has(sourceId)) {
1089
+ add(
1090
+ errors,
1091
+ `$.portfolio[${index}].sourceIds`,
1092
+ 'unknown-source',
1093
+ `Artifact source ${sourceId} is not declared by the set plan.`,
1094
+ );
1095
+ }
1096
+ }
1097
+ if (artifact?.required === false && !isObject(artifact.justification)) {
1098
+ add(
1099
+ errors,
1100
+ `$.portfolio[${index}].justification`,
1101
+ 'optional-justification-required',
1102
+ 'Optional artifacts require a source-backed justification.',
1103
+ );
1104
+ }
1105
+ for (const sourceId of Array.isArray(artifact?.justification?.sourceIds)
1106
+ ? artifact.justification.sourceIds
1107
+ : []) {
1108
+ if (!sourceIds.has(sourceId) || !artifact.sourceIds?.includes(sourceId)) {
1109
+ add(
1110
+ errors,
1111
+ `$.portfolio[${index}].justification.sourceIds`,
1112
+ 'unknown-source',
1113
+ `Justification source ${sourceId} must be declared by the plan and artifact.`,
1114
+ );
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ for (const [field, identity] of [
1120
+ ['terminology', (entry) => entry?.term],
1121
+ ['statuses', (entry) => entry?.subject],
1122
+ ['numbers', (entry) => entry?.subject],
1123
+ ]) {
1124
+ const seen = new Map();
1125
+ for (const [index, entry] of (Array.isArray(value.ledger?.[field])
1126
+ ? value.ledger[field]
1127
+ : []
1128
+ ).entries()) {
1129
+ const key = identity(entry);
1130
+ if (
1131
+ seen.has(key) &&
1132
+ canonicalStringify(seen.get(key)) !== canonicalStringify(entry)
1133
+ ) {
1134
+ add(
1135
+ errors,
1136
+ `$.ledger.${field}[${index}]`,
1137
+ 'ledger-conflict',
1138
+ `Shared ledger contains conflicting values for ${key}.`,
1139
+ );
1140
+ }
1141
+ seen.set(key, entry);
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ function validateAuthorSetContext(value, errors) {
1147
+ if (!isObject(value.setContext) || !isObject(value.plannedArtifact)) {
1148
+ return;
1149
+ }
1150
+ const planned = Array.isArray(value.setContext.portfolio)
1151
+ ? value.setContext.portfolio.find(
1152
+ ({ artifactId }) => artifactId === value.artifactId,
1153
+ )
1154
+ : undefined;
1155
+ if (
1156
+ !planned ||
1157
+ value.plannedArtifact.artifactId !== value.artifactId ||
1158
+ value.plannedArtifact.artifactType !== value.artifactType
1159
+ ) {
1160
+ add(
1161
+ errors,
1162
+ '$.plannedArtifact',
1163
+ 'set-artifact-mismatch',
1164
+ 'Author request identity must match one artifact in the shared set plan.',
1165
+ );
1166
+ return;
1167
+ }
1168
+ if (!deepEqual(planned, value.plannedArtifact)) {
1169
+ add(
1170
+ errors,
1171
+ '$.plannedArtifact',
1172
+ 'set-plan-drift',
1173
+ 'Author request planned artifact must be identical to the shared set plan entry.',
1174
+ );
1175
+ }
1176
+ }
1177
+
1178
+ function validateVisualAuthoringGuidance(value, errors) {
1179
+ if (typeof value.visualAuthoringGuidance !== 'string') {
1180
+ return;
1181
+ }
1182
+ const guidance = value.visualAuthoringGuidance.toLowerCase();
1183
+ const missing = [
1184
+ 'representation',
1185
+ 'hierarchy',
1186
+ 'responsive navigation',
1187
+ 'table',
1188
+ 'diagram',
1189
+ 'deck',
1190
+ ].filter((topic) => !guidance.includes(topic));
1191
+ if (missing.length > 0) {
1192
+ add(
1193
+ errors,
1194
+ '$.visualAuthoringGuidance',
1195
+ 'malformed-authoring-guidance',
1196
+ `Visual authoring guidance is missing bundled topics: ${missing.join(', ')}.`,
1197
+ );
1198
+ }
1199
+ }
1200
+
1201
+ function validateAuthorGraphSemantics(value, errors) {
1202
+ if (value.graphSemantics === undefined) {
1203
+ return;
1204
+ }
1205
+ if (value.authoring !== 'html' || !Array.isArray(value.graphSemantics)) {
1206
+ add(
1207
+ errors,
1208
+ '$.graphSemantics',
1209
+ 'graph-semantics',
1210
+ 'Planner-owned graph semantics are allowed only for artistic HTML authoring.',
1211
+ );
1212
+ return;
1213
+ }
1214
+ for (const [graphIndex, graph] of value.graphSemantics.entries()) {
1215
+ if (!isObject(graph)) continue;
1216
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
1217
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
1218
+ const nodeIds = nodes.map(({ id }) => id);
1219
+ const edgeIds = edges.map(({ from, to }) => `${from}\0${to}`);
1220
+ if (
1221
+ new Set(nodeIds).size !== nodeIds.length ||
1222
+ new Set(edgeIds).size !== edgeIds.length ||
1223
+ edges.some(
1224
+ ({ from, to }) => !nodeIds.includes(from) || !nodeIds.includes(to),
1225
+ )
1226
+ ) {
1227
+ add(
1228
+ errors,
1229
+ `$.graphSemantics[${graphIndex}]`,
1230
+ 'graph-semantics',
1231
+ 'Graph semantics require unique nodes and edges with declared endpoints.',
1232
+ );
655
1233
  }
656
1234
  }
657
1235
  }