@open-agent-toolkit/cli 0.2.72 → 0.2.74

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 (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,41 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
 
4
- import {
5
- composePublicationTarget,
6
- normalizePublishRoots,
7
- } from './s3-roots.mjs';
8
4
  import { validatePortablePath } from './safe-paths.mjs';
9
- import {
10
- parseCanonicalGithubBlobUrl,
11
- validateCanonicalGithubBlobTuple,
12
- } from './source-backlinks.mjs';
13
5
 
14
6
  const SCHEMA_FILES = {
15
- 'run-request': 'run-request.schema.json',
16
7
  'fact-base': 'fact-base.schema.json',
17
- theme: 'theme.schema.json',
18
8
  manifest: 'manifest.schema.json',
19
- 'build-record': 'build-record.schema.json',
20
- 'durability-evidence': 'durability-evidence.schema.json',
21
- 'publish-request/v1': 'publish-request.v1.schema.json',
22
- 'publish-request/v2': 'publish-request.v2.schema.json',
23
- 'publish-receipt/v1': 'publish-receipt.v1.schema.json',
24
- 'publish-receipt/v2': 'publish-receipt.v2.schema.json',
25
- 'author-request/v2': 'author-request.v2.schema.json',
26
- 'author-request/v3': 'author-request.v3.schema.json',
27
- 'author-result/v2': 'author-result.v2.schema.json',
28
- 'set-plan': 'set-plan.v1.schema.json',
29
- 'visual-review-request': 'visual-review-request.v1.schema.json',
30
- 'visual-review-result': 'visual-review-result.v1.schema.json',
31
- 'visual-review-evidence': 'visual-review-evidence.v1.schema.json',
32
- 'terminal-evidence': 'terminal-evidence.v1.schema.json',
33
- };
34
- const DEFAULT_SCHEMA_KEYS = {
35
- 'author-request': 'author-request/v3',
36
- 'author-result': 'author-result/v2',
37
- 'publish-request': 'publish-request/v2',
38
- 'publish-receipt': 'publish-receipt/v2',
9
+ theme: 'theme.schema.json',
39
10
  };
40
11
 
41
12
  const SCHEMAS = Object.fromEntries(
@@ -49,34 +20,8 @@ const SCHEMAS = Object.fromEntries(
49
20
  const SCHEMAS_BY_ID = new Map(
50
21
  Object.values(SCHEMAS).map((schema) => [schema.$id, schema]),
51
22
  );
52
- // `validateContract` accepts a schema `$id` as its `kind`, but every downstream
53
- // gate matches on the short registry form. `'explainer-kit.publish-request/v1'`
54
- // does not start with `'publish-request'`, so an `$id`-form call skipped the
55
- // default-deny publication-root gate entirely and answered `valid: true` for a
56
- // credential-bearing root. Normalize once, here, so no individual gate has to
57
- // remember to handle both spellings.
58
- const KIND_BY_SCHEMA_ID = new Map(
59
- Object.entries(SCHEMAS).map(([kind, schema]) => [schema.$id, kind]),
60
- );
61
-
62
- function canonicalContractKind(kind) {
63
- return KIND_BY_SCHEMA_ID.get(kind) ?? kind;
64
- }
65
-
66
- const LEGACY_PUBLISH_RECEIPT = 'explainer-kit.publish-receipt/v1';
67
-
68
- /**
69
- * True for every publish-receipt shape except the retained v1 replay form.
70
- *
71
- * Stated as "not v1" rather than "=== v2" on purpose: keying verification to an
72
- * exact version string means a future `publish-receipt/v3` silently skips
73
- * sentinel verification and per-artifact validation, which is the same
74
- * fail-open shape the publication-root gate was rewritten to eliminate. v1 is
75
- * the only version that legitimately lacks these facts.
76
- */
77
- export function isVerifiablePublishReceipt(value) {
78
- return value?.schemaVersion !== LEGACY_PUBLISH_RECEIPT;
79
- }
23
+ const DATE_TIME_PATTERN =
24
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
80
25
  const RAW_SECRET_KEYS = new Set([
81
26
  'accesskey',
82
27
  'accesskeyid',
@@ -91,104 +36,24 @@ const RAW_SECRET_KEYS = new Set([
91
36
  'sessiontoken',
92
37
  'token',
93
38
  ]);
94
- const DATE_TIME_PATTERN =
95
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
96
- const SET_PLAN_RECORD_PATHS = [
97
- 'source/set-plan/request.json',
98
- 'source/set-plan/result.json',
99
- 'source/set-plan/ledger.json',
100
- 'source/set-plan/portfolio.json',
101
- 'source/set-plan/drafts.json',
102
- ];
103
39
 
104
- export function validateContract(kind, value, context = {}) {
105
- const schema = resolveContractSchema(kind, value);
40
+ export function validateContract(kind, value) {
41
+ const schema = SCHEMAS[kind] ?? SCHEMAS_BY_ID.get(kind);
106
42
  if (!schema) {
107
- return {
108
- valid: false,
109
- errors: [
110
- {
111
- path: '$',
112
- code: 'unknown-kind',
113
- message: `Unknown contract kind: ${kind}`,
114
- },
115
- ],
116
- };
43
+ throw new Error(`Unknown contract kind: ${kind}`);
117
44
  }
118
45
 
119
46
  const errors = [];
120
- // Every gate below matches on the short registry form, so an `$id`-form kind
121
- // is normalized before dispatch rather than in each gate.
122
- const canonicalKind = canonicalContractKind(kind);
123
47
  findRawSecrets(value, '$', errors);
124
48
  validateSchema(schema, value, '$', schema, errors);
125
- validateContractPaths(canonicalKind, value, errors);
126
- validatePublicationRoots(canonicalKind, value, errors);
127
- validateCrossRecord(canonicalKind, value, context, errors);
128
- validateSourceBacklinks(canonicalKind, value, errors);
129
- return { valid: errors.length === 0, errors };
130
- }
131
-
132
- function validateSourceBacklinks(kind, value, errors) {
133
- if (kind === 'fact-base' || kind === 'explainer-kit.fact-base/v1') {
134
- const tuples = [
135
- ...(value.sources ?? []),
136
- ...(value.claims ?? []).flatMap((claim) => claim.citations ?? []),
137
- ...(value.unresolvedClaims ?? []).flatMap(
138
- (claim) => claim.citations ?? [],
139
- ),
140
- ];
141
- tuples.forEach((tuple, index) => {
142
- const declaresBacklink = ['repository', 'path', 'lineRange', 'url'].some(
143
- (field) => tuple[field] !== undefined,
144
- );
145
- if (declaresBacklink && !validateCanonicalGithubBlobTuple(tuple)) {
146
- add(
147
- errors,
148
- `$.sourceBacklinks[${index}]`,
149
- 'source-backlink',
150
- 'Must declare one complete canonical GitHub blob backlink tuple.',
151
- );
152
- }
153
- });
154
- return;
155
- }
156
- if (kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1') return;
157
- (value.source?.backlinks ?? []).forEach((entry, index) => {
158
- try {
159
- parseCanonicalGithubBlobUrl(entry.url);
160
- } catch (error) {
161
- add(
162
- errors,
163
- `$.source.backlinks[${index}].url`,
164
- 'source-backlink',
165
- error instanceof Error
166
- ? error.message
167
- : 'Must be a canonical GitHub blob backlink.',
168
- );
169
- }
170
- });
171
- }
172
-
173
- function resolveContractSchema(kind, value) {
174
- if (SCHEMAS[kind]) {
175
- return SCHEMAS[kind];
49
+ validateContractPaths(kind, value, errors);
50
+ if (schema === SCHEMAS['fact-base']) {
51
+ rejectRetiredCitationKeys(value, errors);
176
52
  }
177
- if (SCHEMAS_BY_ID.has(kind)) {
178
- return SCHEMAS_BY_ID.get(kind);
179
- }
180
-
181
- const defaultKey = DEFAULT_SCHEMA_KEYS[kind];
182
- if (!defaultKey) {
183
- return null;
53
+ if (schema === SCHEMAS.manifest) {
54
+ validateManifest(value, errors);
184
55
  }
185
-
186
- const declared = isObject(value)
187
- ? SCHEMAS_BY_ID.get(value.schemaVersion)
188
- : undefined;
189
- return declared?.$id.startsWith(`explainer-kit.${kind}/`)
190
- ? declared
191
- : SCHEMAS[defaultKey];
56
+ return { valid: errors.length === 0, errors };
192
57
  }
193
58
 
194
59
  export function canonicalHash(value) {
@@ -202,9 +67,7 @@ export function canonicalStringify(value) {
202
67
  }
203
68
 
204
69
  function canonicalize(value) {
205
- if (Array.isArray(value)) {
206
- return value.map(canonicalize);
207
- }
70
+ if (Array.isArray(value)) return value.map(canonicalize);
208
71
  if (isObject(value)) {
209
72
  return Object.fromEntries(
210
73
  Object.keys(value)
@@ -215,6 +78,88 @@ function canonicalize(value) {
215
78
  return value;
216
79
  }
217
80
 
81
+ function rejectRetiredCitationKeys(value, errors) {
82
+ for (const [group, claims] of [
83
+ ['claims', value?.claims],
84
+ ['unresolvedClaims', value?.unresolvedClaims],
85
+ ]) {
86
+ for (const [claimIndex, claim] of (claims ?? []).entries()) {
87
+ for (const [citationIndex, citation] of (
88
+ claim?.citations ?? []
89
+ ).entries()) {
90
+ for (const key of [
91
+ 'repository',
92
+ 'revision',
93
+ 'path',
94
+ 'lineRange',
95
+ 'url',
96
+ ]) {
97
+ if (citation?.[key] !== undefined) {
98
+ add(
99
+ errors,
100
+ `$.${group}[${claimIndex}].citations[${citationIndex}].${key}`,
101
+ 'unknown-key',
102
+ `Unknown property ${key}.`,
103
+ );
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ function validateManifest(value, errors) {
112
+ const immutableHashes = isObject(value?.immutableHashes)
113
+ ? value.immutableHashes
114
+ : {};
115
+ if (
116
+ typeof value?.theme?.path === 'string' &&
117
+ !(value.theme.path in immutableHashes)
118
+ ) {
119
+ add(
120
+ errors,
121
+ '$.theme.path',
122
+ 'immutable-package-incomplete',
123
+ 'Theme path must be covered by immutable hashes.',
124
+ );
125
+ }
126
+ if (
127
+ typeof value?.theme?.hash === 'string' &&
128
+ value.theme.hash !== immutableHashes['theme.resolved.json']
129
+ ) {
130
+ add(
131
+ errors,
132
+ '$.theme.hash',
133
+ 'hash-mismatch',
134
+ 'Theme hash must match theme.resolved.json.',
135
+ );
136
+ }
137
+ for (const [index, artifact] of (value?.artifacts ?? []).entries()) {
138
+ if (
139
+ typeof artifact?.contentPath === 'string' &&
140
+ !(artifact.contentPath in immutableHashes)
141
+ ) {
142
+ add(
143
+ errors,
144
+ `$.artifacts[${index}].contentPath`,
145
+ 'immutable-package-incomplete',
146
+ 'Artifact content path must be covered by immutable hashes.',
147
+ );
148
+ }
149
+ if (
150
+ typeof artifact?.hash === 'string' &&
151
+ artifact.hash !== immutableHashes[artifact.contentPath]
152
+ ) {
153
+ add(
154
+ errors,
155
+ `$.artifacts[${index}].hash`,
156
+ 'hash-mismatch',
157
+ 'Artifact hash must match its immutable content hash.',
158
+ );
159
+ }
160
+ }
161
+ }
162
+
218
163
  function validateSchema(schema, value, path, rootSchema, errors) {
219
164
  if (schema.$ref) {
220
165
  if (schema.$ref.endsWith('/safeRelativePath')) {
@@ -235,10 +180,8 @@ function validateSchema(schema, value, path, rootSchema, errors) {
235
180
  validateSchema(resolved.schema, value, path, resolved.root, errors);
236
181
  }
237
182
 
238
- if (schema.allOf) {
239
- for (const child of schema.allOf) {
240
- validateSchema(child, value, path, rootSchema, errors);
241
- }
183
+ for (const child of schema.allOf ?? []) {
184
+ validateSchema(child, value, path, rootSchema, errors);
242
185
  }
243
186
 
244
187
  if (schema.oneOf) {
@@ -368,9 +311,7 @@ function validateSchema(schema, value, path, rootSchema, errors) {
368
311
  }
369
312
  }
370
313
  for (const [key, childValue] of Object.entries(value)) {
371
- if (key in properties) {
372
- continue;
373
- }
314
+ if (key in properties) continue;
374
315
  if (schema.additionalProperties === false) {
375
316
  add(
376
317
  errors,
@@ -403,193 +344,20 @@ function validateSchema(schema, value, path, rootSchema, errors) {
403
344
  }
404
345
 
405
346
  function validateContractPaths(kind, value, errors) {
406
- if (!isObject(value)) {
407
- return;
408
- }
409
-
410
- if (kind === 'run-request') {
411
- addLexicalPathErrors(value.outputRoot, '$.outputRoot', errors, true);
412
- if (isObject(value.factBase)) {
413
- addLexicalPathErrors(
414
- value.factBase.path,
415
- '$.factBase.path',
416
- errors,
417
- true,
418
- );
419
- }
420
- if (isObject(value.theme)) {
421
- addLexicalPathErrors(
422
- value.theme.suppliedBundlePath,
423
- '$.theme.suppliedBundlePath',
424
- errors,
425
- true,
426
- );
427
- }
428
- if (isObject(value.durability) && isObject(value.durability.publish)) {
429
- validateContractPaths(
430
- 'publish-request',
431
- value.durability.publish,
432
- errors,
433
- );
434
- }
435
- }
436
-
437
- if (kind === 'publish-request') {
438
- addLexicalPathErrors(value.siteRoot, '$.siteRoot', errors, true);
439
- addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
440
- }
441
-
442
- if (kind === 'durability-evidence') {
443
- addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
444
- if (isObject(value.evidence)) {
445
- addLexicalPathErrors(
446
- value.evidence.repoRoot,
447
- '$.evidence.repoRoot',
448
- errors,
449
- true,
450
- );
451
- addLexicalPathErrors(
452
- value.evidence.receiptPath,
453
- '$.evidence.receiptPath',
454
- errors,
455
- true,
456
- );
457
- if (Array.isArray(value.evidence.paths)) {
458
- value.evidence.paths.forEach((candidate, index) =>
459
- addLexicalPathErrors(
460
- candidate,
461
- `$.evidence.paths[${index}]`,
462
- errors,
463
- false,
464
- ),
465
- );
466
- }
467
- }
468
- }
469
-
470
- if (kind === 'manifest' && Array.isArray(value.artifacts)) {
471
- value.artifacts.forEach((artifact, index) => {
472
- if (isObject(artifact) && isObject(artifact.rebuild)) {
473
- addLexicalPathErrors(
474
- artifact.rebuild.cwd,
475
- `$.artifacts[${index}].rebuild.cwd`,
476
- errors,
477
- true,
478
- );
479
- }
480
- });
481
- }
482
- }
483
-
484
- function validatePublicationRoots(kind, value, errors, path = '$') {
485
- if (!isObject(value)) return;
486
-
487
- if (kind === 'run-request') {
488
- const publish = value.durability?.publish;
489
- if (isObject(publish)) {
490
- validatePublicationRoots(
491
- 'publish-request',
492
- publish,
493
- errors,
494
- `${path}.durability.publish`,
495
- );
496
- }
497
- return;
498
- }
499
-
500
- // Default-deny: every publish-request shape is validated, regardless of its
501
- // declared schemaVersion. Keying this gate to an exact version string is what
502
- // let `publish-request/v1` reach `initializeRun` with credential-bearing roots,
503
- // and would let any future version reintroduce the same bypass.
504
- if (kind.startsWith('publish-request')) {
505
- try {
506
- normalizePublishRoots(value.s3Uri, value.publicBaseUrl);
507
- } catch {
508
- add(
509
- errors,
510
- path,
511
- 'publish-roots',
512
- 'Publish request roots must be credential-free and canonical.',
513
- );
514
- }
347
+ if (
348
+ !isObject(value) ||
349
+ (kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1')
350
+ ) {
515
351
  return;
516
352
  }
517
-
518
- if (kind.startsWith('publish-receipt')) {
519
- // Same default-deny rule as the request branch above: every receipt shape
520
- // has its roots screened, whatever version it declares. Pinning this to the
521
- // exact v2 string let a `publish-receipt/v1` carry credential-bearing and
522
- // internal-address roots straight through to `publish-receipt.json`, a
523
- // retained hash-covered member of the run package, and on into the
524
- // `publish-summary/v1` projection.
525
- const receiptV2 = isVerifiablePublishReceipt(value);
526
- let roots;
527
- try {
528
- roots = normalizePublishRoots(
529
- value.roots?.s3Uri,
530
- value.roots?.publicBaseUrl,
531
- );
532
- // The stricter *canonical-form* assertion stays v2-only: v1 is retained
533
- // purely for replay and may legitimately carry non-canonical historical
534
- // forms such as trailing slashes. Credential and address screening above
535
- // applies to both, because no replay need justifies either.
536
- if (
537
- receiptV2 &&
538
- (roots.s3Uri !== value.roots.s3Uri ||
539
- roots.publicBaseUrl !== value.roots.publicBaseUrl)
540
- ) {
541
- throw new Error('noncanonical');
542
- }
543
- } catch {
544
- add(
545
- errors,
546
- `${path}.roots`,
547
- 'publish-roots',
548
- 'Publish receipt roots must be credential-free and canonical.',
549
- );
550
- return;
551
- }
552
-
553
- if (!receiptV2) return;
554
-
555
- for (const [index, artifact] of (Array.isArray(value.artifacts)
556
- ? value.artifacts
557
- : []
558
- ).entries()) {
559
- if (!isObject(artifact) || typeof artifact.relativePath !== 'string') {
560
- continue;
561
- }
562
- const publishPath = artifact.relativePath.startsWith('site/')
563
- ? artifact.relativePath.slice('site/'.length)
564
- : artifact.relativePath;
565
- try {
566
- const expected = composePublicationTarget(publishPath, roots);
567
- if (
568
- artifact.s3Uri !== expected.s3Uri ||
569
- artifact.publicUrl !== expected.publicUrl
570
- ) {
571
- throw new Error('destination mismatch');
572
- }
573
- } catch {
574
- add(
575
- errors,
576
- `${path}.artifacts[${index}]`,
577
- 'receipt-artifact-destination',
578
- 'Publish receipt artifact destinations must be canonical children of the validated roots.',
579
- );
580
- }
581
- }
353
+ for (const [path, hash] of Object.entries(value.immutableHashes ?? {})) {
354
+ addLexicalPathErrors(path, `$.immutableHashes.${path}`, errors, false);
355
+ if (typeof hash !== 'string') continue;
582
356
  }
583
357
  }
584
358
 
585
359
  function addLexicalPathErrors(value, path, errors, allowAbsolute) {
586
- if (value === undefined) {
587
- return;
588
- }
589
- if (typeof value !== 'string') {
590
- return;
591
- }
592
-
360
+ if (value === undefined || typeof value !== 'string') return;
593
361
  const result = validatePortablePath(value, { allowAbsolute });
594
362
  if (!result.valid) {
595
363
  add(errors, path, 'unsafe-path', result.errors[0].message);
@@ -603,1180 +371,21 @@ function resolveReference(reference, rootSchema) {
603
371
  .split('/')
604
372
  .map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
605
373
  let schema = rootSchema;
606
- for (const part of parts) {
607
- schema = schema?.[part];
608
- }
374
+ for (const part of parts) schema = schema?.[part];
609
375
  return schema ? { schema, root: rootSchema } : null;
610
376
  }
611
377
  const external = SCHEMAS_BY_ID.get(reference);
612
378
  return external ? { schema: external, root: external } : null;
613
379
  }
614
380
 
615
- function validateCrossRecord(kind, value, context, errors) {
616
- if (!isObject(value)) {
617
- return;
618
- }
619
-
620
- if (kind === 'run-request') {
621
- const factBase = value.factBase;
622
- if (isObject(factBase)) {
623
- const hasPath = typeof factBase.path === 'string';
624
- const hasSources = Array.isArray(factBase.sources);
625
- if (
626
- (factBase.mode === 'supplied' && (!hasPath || hasSources)) ||
627
- (factBase.mode === 'federated' && (!hasSources || hasPath))
628
- ) {
629
- add(
630
- errors,
631
- '$.factBase',
632
- 'fact-base-fields',
633
- 'Supplied fact bases require only path; federated fact bases require only sources.',
634
- );
635
- }
636
- }
637
-
638
- const durability = value.durability;
639
- if (
640
- isObject(durability) &&
641
- durability.strategy === 'publish' &&
642
- !isObject(durability.publish)
643
- ) {
644
- add(
645
- errors,
646
- '$.durability.publish',
647
- 'incomplete-publish',
648
- 'Publish durability requires a complete publish request.',
649
- );
650
- }
651
- if (
652
- isObject(durability) &&
653
- durability.strategy !== 'publish' &&
654
- 'publish' in durability
655
- ) {
656
- add(
657
- errors,
658
- '$.durability.publish',
659
- 'unexpected-publish',
660
- 'Publish settings are allowed only for publish durability.',
661
- );
662
- }
663
-
664
- if (
665
- isObject(value.privacy) &&
666
- value.privacy.retainRawArtDirection === true &&
667
- (!isObject(value.theme) ||
668
- typeof value.theme.artDirection !== 'string' ||
669
- value.theme.artDirection.length === 0)
670
- ) {
671
- add(
672
- errors,
673
- '$.privacy.retainRawArtDirection',
674
- 'art-direction-required',
675
- 'Retaining raw art direction requires theme.artDirection.',
676
- );
677
- }
678
-
679
- if (value.recapMode !== undefined && value.recipe?.id !== 'project-recap') {
680
- add(
681
- errors,
682
- '$.recapMode',
683
- 'recap-mode-recipe',
684
- 'recapMode is allowed only for the project-recap recipe.',
685
- );
686
- }
687
- }
688
-
689
- if (kind === 'set-plan') {
690
- validateSetPlan(value, errors);
691
- }
692
-
693
- if (
694
- ['author-request', 'author-request/v2', 'author-request/v3'].includes(
695
- kind,
696
- ) ||
697
- [
698
- 'explainer-kit.author-request/v2',
699
- 'explainer-kit.author-request/v3',
700
- ].includes(value.schemaVersion)
701
- ) {
702
- validateAuthorSetContext(value, errors);
703
- validateVisualAuthoringGuidance(value, errors);
704
- validateAuthorGraphSemantics(value, errors);
705
- if (value.schemaVersion === 'explainer-kit.author-request/v3') {
706
- validateAuthorArtifactLinks(value, errors);
707
- }
708
- }
709
-
710
- if (kind === 'visual-review-request') {
711
- if (
712
- typeof value.requestHash === 'string' &&
713
- value.requestHash !== canonicalHash(visualReviewRequestPayload(value))
714
- ) {
715
- add(
716
- errors,
717
- '$.requestHash',
718
- 'request-hash-mismatch',
719
- 'Visual review request hash does not match its canonical evidence payload.',
720
- );
721
- }
722
- if (
723
- typeof value.requestHash === 'string' &&
724
- typeof value.requestId === 'string' &&
725
- value.requestId !== visualReviewRequestId(value.requestHash)
726
- ) {
727
- add(
728
- errors,
729
- '$.requestId',
730
- 'request-id-mismatch',
731
- 'Visual review request identity does not match its canonical request hash.',
732
- );
733
- }
734
- const plannedArtifactIds = Array.isArray(value.plan?.portfolio)
735
- ? value.plan.portfolio.map(({ artifactId }) => artifactId)
736
- : [];
737
- const plannedIds = new Set(plannedArtifactIds);
738
- const renderedIds = new Set();
739
- const requiresObservedCohesion = value.plan?.recipe?.id === 'project-recap';
740
- const expectedCohesion = expectedLedgerClaims(value.plan?.ledger);
741
- const observedCohesion = new Set();
742
- if (
743
- requiresObservedCohesion &&
744
- ['terminology', 'statuses', 'numericClaims'].some(
745
- (group) => expectedCohesion[group].size === 0,
746
- )
747
- ) {
748
- add(
749
- errors,
750
- '$.plan.ledger',
751
- 'cohesion-ledger-empty',
752
- 'Adaptive recap review requires non-empty terminology, status, and numeric ledger entries.',
753
- );
754
- }
755
- for (const [index, artifact] of (Array.isArray(value.renderedArtifacts)
756
- ? value.renderedArtifacts
757
- : []
758
- ).entries()) {
759
- for (const [evidenceIndex, evidence] of (Array.isArray(artifact?.evidence)
760
- ? artifact.evidence
761
- : []
762
- ).entries()) {
763
- if (evidence?.captureIdentity !== value.captureIdentity) {
764
- add(
765
- errors,
766
- `$.renderedArtifacts[${index}].evidence[${evidenceIndex}].captureIdentity`,
767
- 'browser-runtime-mismatch',
768
- 'Every visual-review evidence record must bind the request browser capture identity.',
769
- );
770
- }
771
- }
772
- if (!plannedIds.has(artifact?.artifactId)) {
773
- add(
774
- errors,
775
- `$.renderedArtifacts[${index}].artifactId`,
776
- 'unknown-artifact',
777
- 'Rendered artifact is not present in the shared set plan.',
778
- );
779
- }
780
- if (renderedIds.has(artifact?.artifactId)) {
781
- add(
782
- errors,
783
- `$.renderedArtifacts[${index}].artifactId`,
784
- 'duplicate-artifact',
785
- 'Rendered artifact IDs must be unique.',
786
- );
787
- }
788
- renderedIds.add(artifact?.artifactId);
789
- const observations = Array.isArray(artifact?.cohesionObservations)
790
- ? artifact.cohesionObservations
791
- : [];
792
- if (requiresObservedCohesion && observations.length === 0) {
793
- add(
794
- errors,
795
- `$.renderedArtifacts[${index}].cohesionObservations`,
796
- 'cohesion-observations-empty',
797
- 'Every adaptive recap artifact must expose observed shared-ledger evidence.',
798
- );
799
- }
800
- for (const [observationIndex, observation] of observations.entries()) {
801
- if (
802
- observation?.artifactId !== artifact?.artifactId ||
803
- observation?.contentHash !== artifact?.renderedHash
804
- ) {
805
- add(
806
- errors,
807
- `$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
808
- 'cohesion-binding-mismatch',
809
- 'Cohesion observations must bind to their artifact and exact rendered content hash.',
810
- );
811
- continue;
812
- }
813
- const expected = expectedCohesion[observation.group]?.get(
814
- observation.claim,
815
- );
816
- if (
817
- expected === undefined ||
818
- normalizeComparable(expected) !==
819
- normalizeComparable(observation.value)
820
- ) {
821
- add(
822
- errors,
823
- `$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
824
- 'cohesion-contradiction',
825
- 'Observed cohesion evidence must match an applicable shared-ledger value.',
826
- );
827
- continue;
828
- }
829
- observedCohesion.add(`${observation.group}:${observation.claim}`);
830
- }
831
- }
832
- for (const artifactId of plannedArtifactIds) {
833
- if (!renderedIds.has(artifactId)) {
834
- add(
835
- errors,
836
- '$.renderedArtifacts',
837
- 'missing-artifact',
838
- `Rendered review set is missing planned artifact ${artifactId}.`,
839
- );
840
- }
841
- }
842
- if (requiresObservedCohesion) {
843
- for (const [group, claims] of Object.entries(expectedCohesion)) {
844
- for (const claim of claims.keys()) {
845
- if (!observedCohesion.has(`${group}:${claim}`)) {
846
- add(
847
- errors,
848
- '$.renderedArtifacts',
849
- 'cohesion-claim-unobserved',
850
- `Shared-ledger claim ${group}.${claim} is not observable in the rendered set.`,
851
- );
852
- }
853
- }
854
- }
855
- }
856
- }
857
-
858
- if (kind === 'visual-review-result') {
859
- const reviewRequest = context.visualReviewRequest;
860
- if (!isObject(reviewRequest)) {
861
- add(
862
- errors,
863
- '$',
864
- 'review-request-required',
865
- 'Visual review results must be validated with their reviewed request.',
866
- );
867
- return;
868
- }
869
-
870
- const requestValidation = validateContract(
871
- 'visual-review-request',
872
- reviewRequest,
873
- );
874
- if (!requestValidation.valid) {
875
- add(
876
- errors,
877
- '$',
878
- 'invalid-review-request',
879
- 'Visual review result cannot bind to an invalid reviewed request.',
880
- );
881
- }
882
- if (
883
- value.requestId !== reviewRequest.requestId ||
884
- value.requestHash !== reviewRequest.requestHash
885
- ) {
886
- add(
887
- errors,
888
- '$.requestHash',
889
- 'review-binding-mismatch',
890
- 'Visual review result must echo the exact reviewed request identity and hash.',
891
- );
892
- }
893
-
894
- const reviewedArtifactIds = Array.isArray(reviewRequest.renderedArtifacts)
895
- ? reviewRequest.renderedArtifacts.map(({ artifactId }) => artifactId)
896
- : [];
897
- const reviewedIds = new Set(reviewedArtifactIds);
898
- const resultArtifactIds = Array.isArray(value.artifactIds)
899
- ? value.artifactIds
900
- : [];
901
- const resultIds = new Set(resultArtifactIds);
902
- if (
903
- resultIds.size !== reviewedIds.size ||
904
- reviewedArtifactIds.some((artifactId) => !resultIds.has(artifactId))
905
- ) {
906
- add(
907
- errors,
908
- '$.artifactIds',
909
- 'review-set-mismatch',
910
- 'Visual review result artifact IDs must equal the complete reviewed request set.',
911
- );
912
- }
913
- for (const [index, finding] of (Array.isArray(value.findings)
914
- ? value.findings
915
- : []
916
- ).entries()) {
917
- if (!reviewedIds.has(finding?.artifactId)) {
918
- add(
919
- errors,
920
- `$.findings[${index}].artifactId`,
921
- 'detached-finding',
922
- 'Visual review findings must reference an artifact in the reviewed request.',
923
- );
924
- }
925
- }
926
- const findingCount = Array.isArray(value.findings)
927
- ? value.findings.length
928
- : 0;
929
- if (
930
- (value.disposition === 'pass' && findingCount > 0) ||
931
- (['correct', 'fail'].includes(value.disposition) && findingCount === 0)
932
- ) {
933
- add(
934
- errors,
935
- '$.disposition',
936
- 'disposition-findings-mismatch',
937
- 'Pass requires no findings; correct and fail require at least one correction finding.',
938
- );
939
- }
940
- }
941
-
942
- if (kind === 'terminal-evidence') {
943
- validateTerminalEvidence(value, context.manifest, errors);
944
- }
945
-
946
- if (kind === 'visual-review-evidence') {
947
- validateVisualReviewEvidence(
948
- value,
949
- context.visualReviewRequest,
950
- context.attempt,
951
- errors,
952
- );
953
- }
954
-
955
- if (kind === 'manifest') {
956
- const paths = [];
957
- for (const artifact of Array.isArray(value.artifacts)
958
- ? value.artifacts
959
- : []) {
960
- if (!isObject(artifact)) {
961
- continue;
962
- }
963
- for (const field of ['contentPath', 'renderedPath']) {
964
- if (typeof artifact[field] === 'string') {
965
- paths.push(artifact[field]);
966
- }
967
- }
968
- if (artifact.status === 'built' && typeof artifact.hash !== 'string') {
969
- add(
970
- errors,
971
- '$.artifacts',
972
- 'built-artifact-hash-required',
973
- 'Built artifacts require a canonical hash.',
974
- );
975
- }
976
- if (artifact.rebuildable === true && !isObject(artifact.rebuild)) {
977
- add(
978
- errors,
979
- '$.artifacts',
980
- 'rebuild-metadata-required',
981
- 'Rebuildable artifacts require rebuild metadata.',
982
- );
983
- }
984
- if (
985
- value.outcome === 'built-durable' &&
986
- artifact.status === 'built' &&
987
- (!Array.isArray(artifact.durableEvidence) ||
988
- artifact.durableEvidence.length === 0)
989
- ) {
990
- add(
991
- errors,
992
- '$.artifacts',
993
- 'durability-evidence-required',
994
- 'Durable built artifacts require durability evidence.',
995
- );
996
- }
997
- }
998
- if (new Set(paths).size !== paths.length) {
999
- add(
1000
- errors,
1001
- '$.artifacts',
1002
- 'duplicate-artifact-path',
1003
- 'Artifact content and rendered paths must be unique.',
1004
- );
1005
- }
1006
-
1007
- const requiredProvenance = [
1008
- 'run-request.json',
1009
- 'source/content-approval.json',
1010
- ];
1011
- const recordedImmutable = isObject(value.immutableHashes)
1012
- ? new Set(Object.keys(value.immutableHashes))
1013
- : new Set();
1014
- const retainsSetPlan =
1015
- value.recipe?.id === 'project-recap' ||
1016
- SET_PLAN_RECORD_PATHS.some((path) => recordedImmutable.has(path));
1017
- const expectedImmutable = new Set([
1018
- ...requiredProvenance,
1019
- value.source?.factBasePath,
1020
- 'source/fact-base.md',
1021
- ...(retainsSetPlan ? SET_PLAN_RECORD_PATHS : []),
1022
- ...(Array.isArray(value.source?.authorResultPaths)
1023
- ? value.source.authorResultPaths
1024
- : []),
1025
- value.theme?.path,
1026
- ...(Array.isArray(value.artifacts)
1027
- ? value.artifacts.flatMap((artifact) => [
1028
- artifact?.contentPath,
1029
- ...(artifact?.status === 'built' &&
1030
- typeof artifact?.renderedPath === 'string'
1031
- ? [artifact.renderedPath]
1032
- : []),
1033
- ])
1034
- : []),
1035
- ]);
1036
- expectedImmutable.delete(undefined);
1037
- const missingLegacyPaths = requiredProvenance.filter(
1038
- (path) => !recordedImmutable.has(path),
1039
- );
1040
- if (missingLegacyPaths.length > 0) {
1041
- add(
1042
- errors,
1043
- '$.immutableHashes',
1044
- 'legacy-manifest-incomplete',
1045
- `Legacy manifest is missing immutable coverage for ${missingLegacyPaths.join(', ')}; regenerate the recap package before archival.`,
1046
- );
1047
- }
1048
- if ([...expectedImmutable].some((path) => !recordedImmutable.has(path))) {
1049
- add(
1050
- errors,
1051
- '$.immutableHashes',
1052
- 'immutable-package-incomplete',
1053
- 'Manifest immutable hashes must cover the complete retained fact-base, set plan, content, theme, and required built artifact package.',
1054
- );
1055
- }
1056
- const hasVisualEvidence = [...recordedImmutable].some((path) =>
1057
- path.startsWith('qa/visual-review/'),
1058
- );
1059
- if (
1060
- hasVisualEvidence &&
1061
- [
1062
- 'qa/visual-review/attempt-1/request.json',
1063
- 'qa/visual-review/attempt-1/result.json',
1064
- ].some((path) => !recordedImmutable.has(path))
1065
- ) {
1066
- add(
1067
- errors,
1068
- '$.immutableHashes',
1069
- 'visual-review-chain-incomplete',
1070
- 'Retained visual review evidence requires its bound attempt request and result.',
1071
- );
1072
- }
1073
- for (const path of recordedImmutable) {
1074
- if (
1075
- (path.startsWith('qa/browser/') ||
1076
- path.includes('/visual-review/attempt-')) &&
1077
- path.endsWith('.png') &&
1078
- !recordedImmutable.has(path.replace(/\.png$/, '.json'))
1079
- ) {
1080
- add(
1081
- errors,
1082
- '$.immutableHashes',
1083
- 'visual-review-chain-incomplete',
1084
- `Screenshot evidence ${path} is missing its immutable metrics record.`,
1085
- );
1086
- }
1087
- }
1088
-
1089
- const record = context.buildRecord;
1090
- if (isObject(record)) {
1091
- if (value.runId !== record.runId || value.outcome !== record.outcome) {
1092
- add(
1093
- errors,
1094
- '$',
1095
- 'cross-record-mismatch',
1096
- 'Manifest and build record identity or outcome do not match.',
1097
- );
1098
- }
1099
- if (
1100
- isObject(value.buildRecord) &&
1101
- value.buildRecord.hash !== canonicalHash(record)
1102
- ) {
1103
- add(
1104
- errors,
1105
- '$.buildRecord.hash',
1106
- 'hash-mismatch',
1107
- 'Build record hash does not match canonical content.',
1108
- );
1109
- }
1110
- }
1111
- if (
1112
- isObject(context.theme) &&
1113
- isObject(value.theme) &&
1114
- value.theme.hash !== canonicalHash(context.theme)
1115
- ) {
1116
- add(
1117
- errors,
1118
- '$.theme.hash',
1119
- 'hash-mismatch',
1120
- 'Theme hash does not match canonical content.',
1121
- );
1122
- }
1123
- if (isObject(context.runRequest) && isObject(record)) {
1124
- const expected =
1125
- isObject(context.runRequest.theme) &&
1126
- typeof context.runRequest.theme.renderStrategy === 'string'
1127
- ? context.runRequest.theme.renderStrategy
1128
- : 'default-only';
1129
- if (record.renderStrategy !== expected) {
1130
- add(
1131
- errors,
1132
- '$.renderStrategy',
1133
- 'cross-record-mismatch',
1134
- 'Build render strategy does not match the run request.',
1135
- );
1136
- }
1137
- }
1138
- }
1139
-
1140
- if (kind.startsWith('publish-receipt') && isVerifiablePublishReceipt(value)) {
1141
- const sentinel = value.sentinel;
1142
- if (
1143
- sentinel?.objectVerification?.status !== 'verified' ||
1144
- sentinel.objectVerification.hash === undefined
1145
- ) {
1146
- add(
1147
- errors,
1148
- '$.sentinel.objectVerification',
1149
- 'receipt-object-verification',
1150
- 'Publish receipt sentinel requires exact authenticated object verification.',
1151
- );
1152
- }
1153
- const validPublic =
1154
- value.publicAccess === 'public'
1155
- ? sentinel?.publicVerification?.status === 'verified' &&
1156
- sentinel.publicVerification.hash === sentinel.objectVerification?.hash
1157
- : sentinel?.publicVerification?.status === 'skipped-protected';
1158
- if (!validPublic) {
1159
- add(
1160
- errors,
1161
- '$.sentinel.publicVerification',
1162
- 'receipt-public-verification',
1163
- 'Publish receipt sentinel public verification must match the declared public-access mode.',
1164
- );
1165
- }
1166
- }
1167
-
1168
- if (
1169
- kind === 'publish-receipt' &&
1170
- isObject(context.manifest) &&
1171
- Array.isArray(value.artifacts)
1172
- ) {
1173
- const expected = new Map();
1174
- for (const artifact of context.manifest.artifacts ?? []) {
1175
- if (
1176
- isObject(artifact) &&
1177
- artifact.status === 'built' &&
1178
- typeof artifact.renderedPath === 'string'
1179
- ) {
1180
- expected.set(artifact.renderedPath, {
1181
- hash: artifact.hash,
1182
- source: { kind: 'manifest', artifactId: artifact.id },
1183
- });
1184
- }
1185
- }
1186
- if (
1187
- isObject(context.catalogArtifact) &&
1188
- typeof context.catalogArtifact.relativePath === 'string'
1189
- ) {
1190
- expected.set(context.catalogArtifact.relativePath, {
1191
- hash: context.catalogArtifact.hash,
1192
- source: { kind: 'auxiliary', name: 'catalog' },
1193
- });
1194
- }
1195
- const received = new Set();
1196
- for (const artifact of value.artifacts) {
1197
- const expectedArtifact = expected.get(artifact?.relativePath);
1198
- if (isObject(artifact) && expectedArtifact?.hash !== artifact.hash) {
1199
- add(
1200
- errors,
1201
- '$.artifacts',
1202
- 'cross-record-mismatch',
1203
- 'Publish receipt artifact does not match the manifest.',
1204
- );
1205
- }
1206
- if (isVerifiablePublishReceipt(value) && isObject(artifact)) {
1207
- validatePublishReceiptV2Artifact(
1208
- value,
1209
- artifact,
1210
- expectedArtifact,
1211
- errors,
1212
- );
1213
- }
1214
- if (received.has(artifact?.relativePath)) {
1215
- add(
1216
- errors,
1217
- '$.artifacts',
1218
- 'receipt-artifact-parity',
1219
- 'Publish receipt artifact paths must be unique.',
1220
- );
1221
- }
1222
- received.add(artifact?.relativePath);
1223
- }
1224
- if (
1225
- received.size !== expected.size ||
1226
- [...expected.keys()].some((path) => !received.has(path))
1227
- ) {
1228
- add(
1229
- errors,
1230
- '$.artifacts',
1231
- 'receipt-artifact-parity',
1232
- 'Publish receipt must exactly cover every manifest artifact and the generated catalog.',
1233
- );
1234
- }
1235
- }
1236
- }
1237
-
1238
- function validateTerminalEvidence(value, manifest, errors) {
1239
- const reasons = Array.isArray(value.reasons) ? value.reasons : [];
1240
- const artifactIds = isObject(manifest)
1241
- ? new Set(
1242
- (Array.isArray(manifest.artifacts) ? manifest.artifacts : []).map(
1243
- ({ id }) => id,
1244
- ),
1245
- )
1246
- : null;
1247
- validateReasonSet(reasons, artifactIds, '$.reasons', errors);
1248
-
1249
- if (isObject(manifest)) {
1250
- if (value.runId !== manifest.runId) {
1251
- add(
1252
- errors,
1253
- '$.runId',
1254
- 'terminal-run-mismatch',
1255
- 'Terminal evidence run identity must match its manifest.',
1256
- );
1257
- }
1258
- if (value.outcome !== manifest.outcome) {
1259
- add(
1260
- errors,
1261
- '$.outcome',
1262
- 'terminal-outcome-mismatch',
1263
- 'Terminal evidence outcome must match its manifest.',
1264
- );
1265
- }
1266
- if (value.manifestHash !== canonicalHash(manifest)) {
1267
- add(
1268
- errors,
1269
- '$.manifestHash',
1270
- 'terminal-manifest-mismatch',
1271
- 'Terminal evidence manifest hash must match its manifest.',
1272
- );
1273
- }
1274
- }
1275
-
1276
- if (value.evidenceDisposition === 'superseded') {
1277
- const reason = reasons[0];
1278
- if (
1279
- reasons.length !== 1 ||
1280
- reason?.stage !== 'finalization' ||
1281
- reason?.kind !== 'superseded' ||
1282
- reason?.count !== 1 ||
1283
- reason?.artifactId !== undefined
1284
- ) {
1285
- add(
1286
- errors,
1287
- '$.reasons',
1288
- 'supersession-reason',
1289
- 'Superseded evidence requires exactly one count-1 finalization/superseded reason without an artifact ID.',
1290
- );
1291
- }
1292
- if (
1293
- !isObject(value.supersededBy) ||
1294
- value.supersededBy.runId === value.runId
1295
- ) {
1296
- add(
1297
- errors,
1298
- '$.supersededBy',
1299
- 'supersession-binding',
1300
- 'Superseded evidence requires a distinct replacement run identity.',
1301
- );
1302
- }
1303
- if (typeof value.manifestHash !== 'string') {
1304
- add(
1305
- errors,
1306
- '$.manifestHash',
1307
- 'supersession-manifest',
1308
- 'Superseded evidence requires the original manifest hash.',
1309
- );
1310
- }
1311
- return;
1312
- }
1313
-
1314
- if (value.supersededBy !== undefined) {
1315
- add(
1316
- errors,
1317
- '$.supersededBy',
1318
- 'unexpected-supersession',
1319
- 'Only superseded evidence may identify a replacement run.',
1320
- );
1321
- }
1322
- if (reasons.some((reason) => reason?.kind === 'superseded')) {
1323
- add(
1324
- errors,
1325
- '$.reasons',
1326
- 'unexpected-supersession',
1327
- 'Only superseded evidence may contain a superseded reason.',
1328
- );
1329
- }
1330
- const allowedKinds =
1331
- value.outcome === 'failed'
1332
- ? new Set(['provider-failure', 'pipeline-failure'])
1333
- : new Set(['finding', 'provider-failure', 'pipeline-failure']);
1334
- if (!reasons.some((reason) => allowedKinds.has(reason?.kind))) {
1335
- add(
1336
- errors,
1337
- '$.reasons',
1338
- 'terminal-outcome-reasons',
1339
- 'Terminal evidence reasons do not satisfy the terminal outcome.',
1340
- );
1341
- }
1342
- }
1343
-
1344
- function validateVisualReviewEvidence(value, request, attempt, errors) {
1345
- if (!isObject(request)) {
1346
- add(
1347
- errors,
1348
- '$',
1349
- 'review-request-required',
1350
- 'Retained visual evidence requires its adjacent reviewed request.',
1351
- );
1352
- return;
1353
- }
1354
- const requestValidation = validateContract('visual-review-request', request);
1355
- if (!requestValidation.valid) {
1356
- add(
1357
- errors,
1358
- '$',
1359
- 'invalid-review-request',
1360
- 'Retained visual evidence cannot bind to an invalid reviewed request.',
1361
- );
1362
- }
1363
- if (value.requestHash !== request.requestHash) {
1364
- add(
1365
- errors,
1366
- '$.requestHash',
1367
- 'review-binding-mismatch',
1368
- 'Retained visual evidence must bind the adjacent request hash.',
1369
- );
1370
- }
1371
- if (![1, 2].includes(attempt) || value.attempt !== attempt) {
1372
- add(
1373
- errors,
1374
- '$.attempt',
1375
- 'review-attempt-mismatch',
1376
- 'Retained visual evidence attempt must match its attempt directory.',
1377
- );
1378
- }
1379
-
1380
- const reasons = Array.isArray(value.reasons) ? value.reasons : [];
1381
- const artifactIds = new Set(
1382
- (Array.isArray(request.renderedArtifacts)
1383
- ? request.renderedArtifacts
1384
- : []
1385
- ).map(({ artifactId }) => artifactId),
1386
- );
1387
- validateReasonSet(reasons, artifactIds, '$.reasons', errors);
1388
- if (reasons.some((reason) => reason?.stage !== 'visual-review')) {
1389
- add(
1390
- errors,
1391
- '$.reasons',
1392
- 'visual-reason-stage',
1393
- 'Retained visual evidence reasons must use the visual-review stage.',
1394
- );
1395
- }
1396
- const validDisposition =
1397
- (value.disposition === 'pass' && reasons.length === 0) ||
1398
- (value.disposition === 'correct' &&
1399
- reasons.length > 0 &&
1400
- reasons.every((reason) => reason?.kind === 'finding')) ||
1401
- (value.disposition === 'failed' &&
1402
- reasons.length > 0 &&
1403
- reasons.every((reason) =>
1404
- ['provider-failure', 'pipeline-failure'].includes(reason?.kind),
1405
- ));
1406
- if (!validDisposition) {
1407
- add(
1408
- errors,
1409
- '$.reasons',
1410
- 'visual-disposition-reasons',
1411
- 'Retained visual evidence reasons do not satisfy its disposition.',
1412
- );
1413
- }
1414
- }
1415
-
1416
- function validateReasonSet(reasons, artifactIds, path, errors) {
1417
- let total = 0;
1418
- const tuples = new Set();
1419
- for (const [index, reason] of reasons.entries()) {
1420
- if (!isObject(reason)) continue;
1421
- if (Number.isInteger(reason.count)) total += reason.count;
1422
- const tuple = `${reason.stage}\0${reason.kind}\0${reason.artifactId ?? ''}`;
1423
- if (tuples.has(tuple)) {
1424
- add(
1425
- errors,
1426
- `${path}[${index}]`,
1427
- 'duplicate-reason',
1428
- 'Retained reason tuples must be unique.',
1429
- );
1430
- }
1431
- tuples.add(tuple);
1432
- if (
1433
- reason.artifactId !== undefined &&
1434
- artifactIds &&
1435
- !artifactIds.has(reason.artifactId)
1436
- ) {
1437
- add(
1438
- errors,
1439
- `${path}[${index}].artifactId`,
1440
- 'foreign-artifact',
1441
- 'Retained reason artifact IDs must belong to the bound run.',
1442
- );
1443
- }
1444
- }
1445
- if (total > 50) {
1446
- add(
1447
- errors,
1448
- path,
1449
- 'reason-total',
1450
- 'Retained reason counts must total at most 50.',
1451
- );
1452
- }
1453
- }
1454
-
1455
- function validatePublishReceiptV2Artifact(receipt, artifact, expected, errors) {
1456
- if (!expected || !deepEqual(artifact.source, expected.source)) {
1457
- add(
1458
- errors,
1459
- '$.artifacts',
1460
- 'receipt-artifact-source',
1461
- 'Publish receipt source identity does not match the manifest or generated auxiliary object.',
1462
- );
1463
- }
1464
- if (
1465
- artifact.objectVerification?.status !== 'verified' ||
1466
- artifact.objectVerification?.hash !== artifact.hash
1467
- ) {
1468
- add(
1469
- errors,
1470
- '$.artifacts',
1471
- 'receipt-object-verification',
1472
- 'Publish receipt object verification must prove the exact artifact hash.',
1473
- );
1474
- }
1475
- const publicVerification = artifact.publicVerification;
1476
- const validPublic =
1477
- receipt.publicAccess === 'public'
1478
- ? publicVerification?.status === 'verified' &&
1479
- publicVerification?.hash === artifact.hash
1480
- : publicVerification?.status === 'skipped-protected';
1481
- if (!validPublic) {
1482
- add(
1483
- errors,
1484
- '$.artifacts',
1485
- 'receipt-public-verification',
1486
- 'Publish receipt public verification must match the declared public-access mode.',
1487
- );
1488
- }
1489
- }
1490
-
1491
- export function visualReviewRequestPayload(request) {
1492
- const {
1493
- requestId: _requestId,
1494
- requestHash: _requestHash,
1495
- ...payload
1496
- } = request;
1497
- return payload;
1498
- }
1499
-
1500
- export function visualReviewRequestId(requestHash) {
1501
- return `visual-review-${String(requestHash).replace(/^sha256:/, '')}`;
1502
- }
1503
-
1504
- function expectedLedgerClaims(ledger) {
1505
- return {
1506
- terminology: new Map(
1507
- (ledger?.terminology ?? []).map(({ term }) => [term, term]),
1508
- ),
1509
- statuses: new Map(
1510
- (ledger?.statuses ?? []).map(({ subject, value }) => [subject, value]),
1511
- ),
1512
- numericClaims: new Map(
1513
- (ledger?.numbers ?? []).map(({ subject, value }) => [subject, value]),
1514
- ),
1515
- };
1516
- }
1517
-
1518
- function normalizeComparable(value) {
1519
- return String(value).trim().toLocaleLowerCase();
1520
- }
1521
-
1522
- function validateSetPlan(value, errors) {
1523
- const sourceIds = new Set(
1524
- Array.isArray(value.sourceIds) ? value.sourceIds : [],
1525
- );
1526
- const artifactIds = new Set();
1527
- for (const [index, artifact] of (Array.isArray(value.portfolio)
1528
- ? value.portfolio
1529
- : []
1530
- ).entries()) {
1531
- if (artifactIds.has(artifact?.artifactId)) {
1532
- add(
1533
- errors,
1534
- `$.portfolio[${index}].artifactId`,
1535
- 'duplicate-artifact',
1536
- 'Set-plan artifact IDs must be unique.',
1537
- );
1538
- }
1539
- artifactIds.add(artifact?.artifactId);
1540
- for (const sourceId of Array.isArray(artifact?.sourceIds)
1541
- ? artifact.sourceIds
1542
- : []) {
1543
- if (!sourceIds.has(sourceId)) {
1544
- add(
1545
- errors,
1546
- `$.portfolio[${index}].sourceIds`,
1547
- 'unknown-source',
1548
- `Artifact source ${sourceId} is not declared by the set plan.`,
1549
- );
1550
- }
1551
- }
1552
- if (artifact?.required === false && !isObject(artifact.justification)) {
1553
- add(
1554
- errors,
1555
- `$.portfolio[${index}].justification`,
1556
- 'optional-justification-required',
1557
- 'Optional artifacts require a source-backed justification.',
1558
- );
1559
- }
1560
- for (const sourceId of Array.isArray(artifact?.justification?.sourceIds)
1561
- ? artifact.justification.sourceIds
1562
- : []) {
1563
- if (!sourceIds.has(sourceId) || !artifact.sourceIds?.includes(sourceId)) {
1564
- add(
1565
- errors,
1566
- `$.portfolio[${index}].justification.sourceIds`,
1567
- 'unknown-source',
1568
- `Justification source ${sourceId} must be declared by the plan and artifact.`,
1569
- );
1570
- }
1571
- }
1572
- }
1573
-
1574
- for (const [field, identity] of [
1575
- ['terminology', (entry) => entry?.term],
1576
- ['statuses', (entry) => entry?.subject],
1577
- ['numbers', (entry) => entry?.subject],
1578
- ]) {
1579
- const seen = new Map();
1580
- for (const [index, entry] of (Array.isArray(value.ledger?.[field])
1581
- ? value.ledger[field]
1582
- : []
1583
- ).entries()) {
1584
- const key = identity(entry);
1585
- if (
1586
- seen.has(key) &&
1587
- canonicalStringify(seen.get(key)) !== canonicalStringify(entry)
1588
- ) {
1589
- add(
1590
- errors,
1591
- `$.ledger.${field}[${index}]`,
1592
- 'ledger-conflict',
1593
- `Shared ledger contains conflicting values for ${key}.`,
1594
- );
1595
- }
1596
- seen.set(key, entry);
1597
- }
1598
- }
1599
- }
1600
-
1601
- function validateAuthorSetContext(value, errors) {
1602
- if (!isObject(value.setContext) || !isObject(value.plannedArtifact)) {
1603
- return;
1604
- }
1605
- const planned = Array.isArray(value.setContext.portfolio)
1606
- ? value.setContext.portfolio.find(
1607
- ({ artifactId }) => artifactId === value.artifactId,
1608
- )
1609
- : undefined;
1610
- if (
1611
- !planned ||
1612
- value.plannedArtifact.artifactId !== value.artifactId ||
1613
- value.plannedArtifact.artifactType !== value.artifactType
1614
- ) {
1615
- add(
1616
- errors,
1617
- '$.plannedArtifact',
1618
- 'set-artifact-mismatch',
1619
- 'Author request identity must match one artifact in the shared set plan.',
1620
- );
1621
- return;
1622
- }
1623
- if (!deepEqual(planned, value.plannedArtifact)) {
1624
- add(
1625
- errors,
1626
- '$.plannedArtifact',
1627
- 'set-plan-drift',
1628
- 'Author request planned artifact must be identical to the shared set plan entry.',
1629
- );
1630
- }
1631
- }
1632
-
1633
- function validateAuthorArtifactLinks(value, errors) {
1634
- if (!Array.isArray(value.artifactLinks) || !isObject(value.setContext)) {
1635
- return;
1636
- }
1637
- const planned = Array.isArray(value.setContext.portfolio)
1638
- ? value.setContext.portfolio
1639
- : [];
1640
- const linksById = new Map();
1641
- for (const [index, link] of value.artifactLinks.entries()) {
1642
- if (!isObject(link)) continue;
1643
- if (linksById.has(link.artifactId)) {
1644
- add(
1645
- errors,
1646
- `$.artifactLinks[${index}].artifactId`,
1647
- 'artifact-link-parity',
1648
- 'Canonical artifact link IDs must be unique.',
1649
- );
1650
- }
1651
- linksById.set(link.artifactId, link);
1652
- }
1653
- if (
1654
- linksById.size !== planned.length ||
1655
- planned.some(
1656
- ({ artifactId, artifactType }) =>
1657
- linksById.get(artifactId)?.artifactType !== artifactType,
1658
- )
1659
- ) {
1660
- add(
1661
- errors,
1662
- '$.artifactLinks',
1663
- 'artifact-link-parity',
1664
- 'Canonical artifact links must exactly cover the planned portfolio.',
1665
- );
1666
- return;
1667
- }
1668
-
1669
- const current = linksById.get(value.artifactId);
1670
- if (!current || typeof current.sitePath !== 'string') return;
1671
- const base = new URL(current.sitePath, 'https://explainer.invalid/');
1672
- for (const [index, link] of value.artifactLinks.entries()) {
1673
- if (
1674
- !isObject(link) ||
1675
- typeof link.href !== 'string' ||
1676
- typeof link.sitePath !== 'string'
1677
- ) {
1678
- continue;
1679
- }
1680
- let resolved;
1681
- try {
1682
- resolved = new URL(link.href, base);
1683
- } catch {
1684
- add(
1685
- errors,
1686
- `$.artifactLinks[${index}].href`,
1687
- 'artifact-link-resolution',
1688
- 'Canonical artifact href must be a valid relative reference.',
1689
- );
1690
- continue;
1691
- }
1692
- if (
1693
- resolved.origin !== base.origin ||
1694
- resolved.protocol !== 'https:' ||
1695
- resolved.username ||
1696
- resolved.password ||
1697
- resolved.search ||
1698
- resolved.hash ||
1699
- resolved.pathname !== `/${link.sitePath}`
1700
- ) {
1701
- add(
1702
- errors,
1703
- `$.artifactLinks[${index}].href`,
1704
- 'artifact-link-resolution',
1705
- 'Canonical artifact href must resolve exactly to its declared site path.',
1706
- );
1707
- }
1708
- }
1709
- }
1710
-
1711
- function validateVisualAuthoringGuidance(value, errors) {
1712
- if (typeof value.visualAuthoringGuidance !== 'string') {
1713
- return;
1714
- }
1715
- const guidance = value.visualAuthoringGuidance.toLowerCase();
1716
- const missing = [
1717
- 'representation',
1718
- 'hierarchy',
1719
- 'responsive navigation',
1720
- 'table',
1721
- 'diagram',
1722
- 'deck',
1723
- ].filter((topic) => !guidance.includes(topic));
1724
- if (missing.length > 0) {
1725
- add(
1726
- errors,
1727
- '$.visualAuthoringGuidance',
1728
- 'malformed-authoring-guidance',
1729
- `Visual authoring guidance is missing bundled topics: ${missing.join(', ')}.`,
1730
- );
1731
- }
1732
- }
1733
-
1734
- function validateAuthorGraphSemantics(value, errors) {
1735
- if (value.graphSemantics === undefined) {
1736
- return;
1737
- }
1738
- if (value.authoring !== 'html' || !Array.isArray(value.graphSemantics)) {
1739
- add(
1740
- errors,
1741
- '$.graphSemantics',
1742
- 'graph-semantics',
1743
- 'Planner-owned graph semantics are allowed only for artistic HTML authoring.',
1744
- );
1745
- return;
1746
- }
1747
- for (const [graphIndex, graph] of value.graphSemantics.entries()) {
1748
- if (!isObject(graph)) continue;
1749
- const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
1750
- const edges = Array.isArray(graph.edges) ? graph.edges : [];
1751
- const nodeIds = nodes.map(({ id }) => id);
1752
- const edgeIds = edges.map(({ from, to }) => `${from}\0${to}`);
1753
- if (
1754
- new Set(nodeIds).size !== nodeIds.length ||
1755
- new Set(edgeIds).size !== edgeIds.length ||
1756
- edges.some(
1757
- ({ from, to }) => !nodeIds.includes(from) || !nodeIds.includes(to),
1758
- )
1759
- ) {
1760
- add(
1761
- errors,
1762
- `$.graphSemantics[${graphIndex}]`,
1763
- 'graph-semantics',
1764
- 'Graph semantics require unique nodes and edges with declared endpoints.',
1765
- );
1766
- }
1767
- }
1768
- }
1769
-
1770
- function findRawSecrets(value, path, errors) {
1771
- if (Array.isArray(value)) {
1772
- value.forEach((item, index) =>
1773
- findRawSecrets(item, `${path}[${index}]`, errors),
1774
- );
1775
- return;
1776
- }
1777
- if (!isObject(value)) {
381
+ function findRawSecrets(value, path, errors) {
382
+ if (Array.isArray(value)) {
383
+ value.forEach((item, index) =>
384
+ findRawSecrets(item, `${path}[${index}]`, errors),
385
+ );
1778
386
  return;
1779
387
  }
388
+ if (!isObject(value)) return;
1780
389
  for (const [key, child] of Object.entries(value)) {
1781
390
  const normalized = key.toLowerCase().replaceAll(/[^a-z0-9]/g, '');
1782
391
  if (RAW_SECRET_KEYS.has(normalized)) {