@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,643 +0,0 @@
1
- import { execFile as execFileCallback } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import { readFile, realpath } from 'node:fs/promises';
4
- import { dirname, relative, resolve, sep } from 'node:path';
5
- import { promisify } from 'node:util';
6
-
7
- import {
8
- catalogFromManifest,
9
- initiativeCatalogPath,
10
- serializeInitiativeCatalog,
11
- validateInitiativeCatalog,
12
- } from './catalog.mjs';
13
- import {
14
- canonicalHash,
15
- isVerifiablePublishReceipt,
16
- validateContract,
17
- } from './contracts.mjs';
18
- import { writeFileAtomic, writeJsonAtomic } from './fs-safe.mjs';
19
- import { validateSafeRelativePath } from './safe-paths.mjs';
20
-
21
- const execFile = promisify(execFileCallback);
22
- const MUTABLE_RECORDS = new Set(['manifest.json', 'build-record.json']);
23
- const FAILURE_WARNING =
24
- 'E_DURABILITY: durability evidence verification failed.';
25
-
26
- export async function recordDurability(request, options = {}) {
27
- assertValid('durability-evidence', request);
28
-
29
- const manifestPath = resolve(request.manifestPath);
30
- const runRoot = dirname(manifestPath);
31
- if (manifestPath !== joinWithin(runRoot, 'manifest.json')) {
32
- throw new Error('Durability manifestPath must identify manifest.json.');
33
- }
34
-
35
- const manifest = await readJson(manifestPath);
36
- const buildRecordPath = joinWithin(runRoot, manifest.buildRecord?.path);
37
- const buildRecord = await readJson(buildRecordPath);
38
- assertValid('build-record', buildRecord);
39
- assertValid('manifest', manifest, { buildRecord });
40
- if (manifest.outcome === 'built-needs-review') {
41
- throw new Error(
42
- 'built-needs-review requires a passing visual review before durability attestation.',
43
- );
44
- }
45
- if (
46
- manifest.outcome !== 'built-not-durable' &&
47
- manifest.outcome !== 'built-durable'
48
- ) {
49
- throw new Error(
50
- `Durability can only attest built runs, not ${manifest.outcome}.`,
51
- );
52
- }
53
-
54
- const verification = await verifyEvidence(request.evidence, {
55
- runRoot,
56
- manifest,
57
- options,
58
- });
59
- const now = options.now ?? (() => new Date().toISOString());
60
-
61
- if (!verification.verified) {
62
- const errors = verification.errors;
63
- updateOutcomes(manifest, buildRecord, {
64
- durable: false,
65
- warning: `${FAILURE_WARNING} ${errors.map(({ message }) => message).join(' ')}`,
66
- completedAt: now(),
67
- });
68
- await persistRecords(runRoot, manifest, buildRecord);
69
- return {
70
- durable: false,
71
- outcome: 'built-not-durable',
72
- errors,
73
- manifestPath,
74
- buildRecordPath,
75
- };
76
- }
77
-
78
- const attestedAt = now();
79
- const alreadyRecorded =
80
- manifest.outcome === 'built-durable' &&
81
- verification.artifacts.every((artifact) =>
82
- hasEvidence(artifact, verification.evidence),
83
- ) &&
84
- (!verification.publishReceipt ||
85
- manifest.publishReceipt?.hash ===
86
- canonicalHash(verification.publishReceipt));
87
- if (alreadyRecorded) {
88
- return {
89
- durable: true,
90
- outcome: 'built-durable',
91
- errors: [],
92
- manifestPath,
93
- buildRecordPath,
94
- };
95
- }
96
-
97
- for (const artifact of verification.artifacts) {
98
- appendEvidence(artifact, {
99
- ...verification.evidence,
100
- attestedAt,
101
- });
102
- }
103
- if (verification.publishReceipt) {
104
- manifest.publishReceipt = {
105
- path: 'publish-receipt.json',
106
- hash: canonicalHash(verification.publishReceipt),
107
- };
108
- }
109
- updateOutcomes(manifest, buildRecord, {
110
- durable: true,
111
- completedAt: attestedAt,
112
- });
113
- await persistRecords(runRoot, manifest, buildRecord);
114
-
115
- return {
116
- durable: true,
117
- outcome: 'built-durable',
118
- errors: [],
119
- manifestPath,
120
- buildRecordPath,
121
- };
122
- }
123
-
124
- export async function verifyRebuildability(artifact, runRoot) {
125
- if (artifact?.rebuildable !== true) {
126
- return {
127
- verified: false,
128
- reason: 'Artifact rebuildability defaults to false.',
129
- };
130
- }
131
- if (
132
- !artifact.rebuild ||
133
- !Array.isArray(artifact.rebuild.argv) ||
134
- artifact.rebuild.argv.length === 0
135
- ) {
136
- return {
137
- verified: false,
138
- reason: 'Rebuildable artifacts require deterministic replay metadata.',
139
- };
140
- }
141
-
142
- try {
143
- const canonicalRunRoot = await realpath(runRoot);
144
- const cwd = resolve(canonicalRunRoot, artifact.rebuild.cwd);
145
- assertWithin(canonicalRunRoot, cwd, 'Replay cwd');
146
- await realpath(cwd);
147
-
148
- for (const [inputPath, expectedHash] of Object.entries(
149
- artifact.rebuild.inputHashes ?? {},
150
- )) {
151
- const checked = validateSafeRelativePath(inputPath);
152
- if (!checked.valid) {
153
- return {
154
- verified: false,
155
- reason: `Replay input path is unsafe: ${inputPath}.`,
156
- };
157
- }
158
- const absoluteInput = resolve(canonicalRunRoot, checked.normalizedPath);
159
- assertWithin(canonicalRunRoot, absoluteInput, 'Replay input');
160
- if ((await fileHash(absoluteInput)) !== expectedHash) {
161
- return {
162
- verified: false,
163
- reason: `Replay input hash does not match: ${inputPath}.`,
164
- };
165
- }
166
- }
167
-
168
- const outputPath = resolve(canonicalRunRoot, artifact.renderedPath);
169
- assertWithin(canonicalRunRoot, outputPath, 'Replay output');
170
- const original = await readFile(outputPath);
171
- try {
172
- const [command, ...args] = artifact.rebuild.argv;
173
- await execFile(command, args, { cwd, maxBuffer: 10 * 1024 * 1024 });
174
- if ((await fileHash(outputPath)) !== artifact.hash) {
175
- return {
176
- verified: false,
177
- reason: `Deterministic replay hash does not match for ${artifact.renderedPath}.`,
178
- };
179
- }
180
- } finally {
181
- await writeFileAtomic(runRoot, artifact.renderedPath, original);
182
- }
183
- return { verified: true, reason: null };
184
- } catch (caught) {
185
- return {
186
- verified: false,
187
- reason: `Deterministic replay failed: ${errorMessage(caught)}`,
188
- };
189
- }
190
- }
191
-
192
- async function verifyEvidence(evidence, context) {
193
- const immutableErrors = await verifyImmutablePackage(
194
- context.runRoot,
195
- context.manifest,
196
- );
197
- if (immutableErrors.length > 0) {
198
- return { verified: false, errors: immutableErrors };
199
- }
200
- const replayErrors = [];
201
- for (const artifact of context.manifest.artifacts) {
202
- if (artifact.status === 'built' && artifact.rebuildable === true) {
203
- const replay = await verifyRebuildability(artifact, context.runRoot);
204
- if (!replay.verified) {
205
- replayErrors.push(error('replay-verification', replay.reason));
206
- }
207
- }
208
- }
209
- if (replayErrors.length > 0) {
210
- return { verified: false, errors: replayErrors };
211
- }
212
-
213
- return evidence.kind === 'commit'
214
- ? verifyCommitEvidence(evidence, context)
215
- : verifyPublishEvidence(evidence, context);
216
- }
217
-
218
- async function verifyImmutablePackage(runRoot, manifest) {
219
- const errors = [];
220
- for (const [path, expectedHash] of Object.entries(manifest.immutableHashes)) {
221
- try {
222
- const bytes = await readFile(joinWithin(runRoot, path));
223
- const actualHash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
224
- if (actualHash !== expectedHash) {
225
- errors.push(
226
- error('hash-mismatch', `Immutable package hash changed for ${path}.`),
227
- );
228
- }
229
- } catch (caught) {
230
- errors.push(
231
- error(
232
- 'missing-path',
233
- `Immutable package evidence ${path} is unavailable: ${errorMessage(caught)}`,
234
- ),
235
- );
236
- }
237
- }
238
- return errors;
239
- }
240
-
241
- async function verifyCommitEvidence(evidence, { runRoot, manifest }) {
242
- const errors = [];
243
- let repoRoot;
244
- let commit;
245
- try {
246
- repoRoot = await realpath(evidence.repoRoot);
247
- runRoot = await realpath(runRoot);
248
- assertWithin(repoRoot, runRoot, 'Manifest');
249
- ({ stdout: commit } = await execFile(
250
- 'git',
251
- ['rev-parse', '--verify', `${evidence.commit}^{commit}`],
252
- { cwd: repoRoot },
253
- ));
254
- commit = commit.trim();
255
- } catch (caught) {
256
- return {
257
- verified: false,
258
- errors: [error('commit-verification', errorMessage(caught))],
259
- };
260
- }
261
-
262
- const manifestRepoPath = toRepoPath(
263
- repoRoot,
264
- joinWithin(runRoot, 'manifest.json'),
265
- );
266
- const buildRecordRepoPath = toRepoPath(
267
- repoRoot,
268
- joinWithin(runRoot, manifest.buildRecord.path),
269
- );
270
- const mutablePaths = new Set([manifestRepoPath, buildRecordRepoPath]);
271
- for (const path of evidence.paths) {
272
- if (
273
- MUTABLE_RECORDS.has(path) ||
274
- mutablePaths.has(path) ||
275
- path.endsWith('/manifest.json') ||
276
- path.endsWith('/build-record.json')
277
- ) {
278
- errors.push(
279
- error(
280
- 'mutable-record',
281
- `Commit evidence must exclude mutable record ${path}.`,
282
- ),
283
- );
284
- }
285
- }
286
-
287
- const required = immutablePackage(manifest);
288
- const requiredByPath = new Map(
289
- required.map((file) => [
290
- toRepoPath(repoRoot, joinWithin(runRoot, file.path)),
291
- file,
292
- ]),
293
- );
294
- for (const [path] of requiredByPath) {
295
- if (!evidence.paths.includes(path)) {
296
- errors.push(
297
- error(
298
- 'missing-artifact',
299
- `Commit evidence does not include required artifact ${path}.`,
300
- ),
301
- );
302
- }
303
- }
304
-
305
- for (const file of required) {
306
- const localPath = joinWithin(runRoot, file.path);
307
- if ((await fileHash(localPath)) !== file.hash) {
308
- errors.push(
309
- error(
310
- 'hash-mismatch',
311
- `Retained package hash does not match manifest hash for ${file.path}.`,
312
- ),
313
- );
314
- }
315
- }
316
-
317
- for (const path of evidence.paths) {
318
- if (mutablePaths.has(path) || MUTABLE_RECORDS.has(path)) {
319
- continue;
320
- }
321
- try {
322
- const { stdout: blob } = await execFile(
323
- 'git',
324
- ['cat-file', 'blob', `${commit}:${path}`],
325
- { cwd: repoRoot, encoding: 'buffer', maxBuffer: 50 * 1024 * 1024 },
326
- );
327
- const file = requiredByPath.get(path);
328
- if (file && bufferHash(blob) !== file.hash) {
329
- errors.push(
330
- error(
331
- 'hash-mismatch',
332
- `Commit blob hash does not match manifest hash for ${path}.`,
333
- ),
334
- );
335
- }
336
- } catch (caught) {
337
- errors.push(
338
- error(
339
- 'missing-commit-path',
340
- `Commit does not contain ${path}: ${errorMessage(caught)}`,
341
- ),
342
- );
343
- }
344
- }
345
-
346
- return errors.length > 0
347
- ? { verified: false, errors }
348
- : {
349
- verified: true,
350
- errors: [],
351
- artifacts: requiredArtifacts(manifest),
352
- evidence: {
353
- kind: 'commit',
354
- ref: commit,
355
- paths: [...evidence.paths],
356
- },
357
- };
358
- }
359
-
360
- async function verifyPublishEvidence(
361
- evidence,
362
- { runRoot, manifest, options = {} },
363
- ) {
364
- // Injectable for the rejection-branch test below, exactly as `fetchImpl` is
365
- // on `defaultHttpGet`: the branch fires only when the builder and validator
366
- // disagree, which correct production code never does on purpose.
367
- const buildCatalog = options.buildCatalog ?? catalogFromManifest;
368
- const expectedPath = joinWithin(runRoot, 'publish-receipt.json');
369
- if (resolve(evidence.receiptPath) !== expectedPath) {
370
- return {
371
- verified: false,
372
- errors: [
373
- error(
374
- 'receipt-location',
375
- 'Publish receipt must be publish-receipt.json beside the manifest.',
376
- ),
377
- ],
378
- };
379
- }
380
-
381
- let receipt;
382
- try {
383
- receipt = await readJson(expectedPath);
384
- } catch (caught) {
385
- return {
386
- verified: false,
387
- errors: [error('publish-receipt', errorMessage(caught))],
388
- };
389
- }
390
- let catalogArtifact;
391
- if (isVerifiablePublishReceipt(receipt)) {
392
- try {
393
- // Must rebuild the catalog under the policy the receipt declares. The
394
- // connector embedded that policy in the catalog it hashed, so verifying a
395
- // `protected` receipt against a `public`-shaped rebuild yields a
396
- // different hash and rejects every protected publication as
397
- // `cross-record-mismatch`.
398
- const catalog = buildCatalog(manifest, receipt.roots?.publicBaseUrl, {
399
- publicAccess: receipt.publicAccess,
400
- });
401
- // This is a builder/validator cross-check on a catalog rebuilt three
402
- // lines above from the same inputs — durability never consumes catalog
403
- // bytes from outside, only a recorded hash, so no externally-sourced
404
- // catalog object exists here to validate. The value of the check is that
405
- // `catalogFromManifest` and `validateInitiativeCatalog` are independent
406
- // implementations of the same contract: if they ever diverge, this
407
- // reports the specific contract violation instead of letting the
408
- // divergence surface as an opaque `cross-record-mismatch`.
409
- const catalogValidation = validateInitiativeCatalog(
410
- catalog,
411
- manifest,
412
- receipt.roots?.publicBaseUrl,
413
- { publicAccess: receipt.publicAccess },
414
- );
415
- if (!catalogValidation.valid) {
416
- return {
417
- verified: false,
418
- errors: [
419
- error(
420
- 'publish-receipt',
421
- `Rebuilt initiative catalog is invalid: ${catalogValidation.errors[0].message}`,
422
- ),
423
- ],
424
- };
425
- }
426
- catalogArtifact = {
427
- relativePath: initiativeCatalogPath(manifest.slug),
428
- hash: bufferHash(Buffer.from(serializeInitiativeCatalog(catalog))),
429
- };
430
- } catch (caught) {
431
- return {
432
- verified: false,
433
- errors: [error('publish-receipt', errorMessage(caught))],
434
- };
435
- }
436
- }
437
- const validation = validateContract('publish-receipt', receipt, {
438
- manifest,
439
- ...(catalogArtifact && { catalogArtifact }),
440
- });
441
- const errors = validation.errors.map(({ code, message }) =>
442
- error(code, message),
443
- );
444
- if (!sentinelVerificationComplete(receipt)) {
445
- errors.push(
446
- error(
447
- 'sentinel-verification',
448
- 'Publish receipt sentinel verification is incomplete.',
449
- ),
450
- );
451
- }
452
-
453
- const receiptByPath = new Map(
454
- (receipt.artifacts ?? []).map((artifact) => [
455
- artifact.relativePath,
456
- artifact,
457
- ]),
458
- );
459
- const required = requiredArtifacts(manifest);
460
- for (const artifact of required) {
461
- const published = receiptByPath.get(artifact.renderedPath);
462
- if (!published || published.hash !== artifact.hash) {
463
- errors.push(
464
- error(
465
- 'missing-artifact',
466
- `Publish receipt does not verify ${artifact.renderedPath}.`,
467
- ),
468
- );
469
- }
470
- }
471
-
472
- return errors.length > 0
473
- ? { verified: false, errors }
474
- : {
475
- verified: true,
476
- errors: [],
477
- artifacts: required,
478
- publishReceipt: receipt,
479
- evidence: {
480
- kind: 'publish',
481
- ref: canonicalHash(receipt),
482
- paths: required.map(({ renderedPath }) => renderedPath),
483
- },
484
- };
485
- }
486
-
487
- function sentinelVerificationComplete(receipt) {
488
- if (receipt.sentinel?.deleted !== true) return false;
489
- if (receipt.schemaVersion === 'explainer-kit.publish-receipt/v1') {
490
- return (
491
- receipt.sentinel.uploadVerified === true &&
492
- receipt.sentinel.publicVerified === true
493
- );
494
- }
495
- if (receipt.schemaVersion !== 'explainer-kit.publish-receipt/v2') {
496
- return false;
497
- }
498
- return (
499
- receipt.sentinel.objectVerification?.status === 'verified' &&
500
- (receipt.publicAccess === 'public'
501
- ? receipt.sentinel.publicVerification?.status === 'verified'
502
- : receipt.sentinel.publicVerification?.status === 'skipped-protected')
503
- );
504
- }
505
-
506
- function appendEvidence(artifact, evidence) {
507
- artifact.durableEvidence ??= [];
508
- if (hasEvidence(artifact, evidence)) {
509
- return;
510
- }
511
-
512
- const prior = artifact.durableEvidence.findLast(
513
- (existing) => existing.kind === evidence.kind,
514
- );
515
- const next = structuredClone(evidence);
516
- if (
517
- prior &&
518
- (prior.ref !== next.ref || !arraysEqual(prior.paths, next.paths))
519
- ) {
520
- next.supersedes = { ref: prior.ref, paths: [...prior.paths] };
521
- }
522
- artifact.durableEvidence.push(next);
523
- }
524
-
525
- function hasEvidence(artifact, evidence) {
526
- return (artifact.durableEvidence ?? []).some(
527
- (existing) =>
528
- existing.kind === evidence.kind &&
529
- existing.ref === evidence.ref &&
530
- arraysEqual(existing.paths, evidence.paths),
531
- );
532
- }
533
-
534
- function updateOutcomes(
535
- manifest,
536
- buildRecord,
537
- { durable, warning, completedAt = new Date().toISOString() },
538
- ) {
539
- const outcome = durable ? 'built-durable' : 'built-not-durable';
540
- manifest.outcome = outcome;
541
- buildRecord.outcome = outcome;
542
-
543
- manifest.warnings = manifest.warnings.filter(
544
- (entry) => !entry.startsWith('E_DURABILITY:'),
545
- );
546
- const stage = buildRecord.stages.find(({ id }) => id === 'durability');
547
- if (!stage) {
548
- throw new Error('Build record does not contain a durability stage.');
549
- }
550
- stage.status = durable ? 'passed' : 'warned';
551
- stage.completedAt = completedAt;
552
- stage.outputPaths = durable ? ['manifest.json', 'build-record.json'] : [];
553
- stage.warnings = warning ? [warning] : [];
554
- if (warning) {
555
- manifest.warnings.push(warning);
556
- }
557
- }
558
-
559
- async function persistRecords(runRoot, manifest, buildRecord) {
560
- assertValid('build-record', buildRecord);
561
- manifest.buildRecord.hash = canonicalHash(buildRecord);
562
- assertValid('manifest', manifest, { buildRecord });
563
- await writeJsonAtomic(runRoot, 'build-record.json', buildRecord);
564
- await writeJsonAtomic(runRoot, 'manifest.json', manifest);
565
- }
566
-
567
- function requiredArtifacts(manifest) {
568
- return manifest.artifacts.filter(
569
- (artifact) =>
570
- artifact.status === 'built' &&
571
- artifact.rebuildable === false &&
572
- typeof artifact.renderedPath === 'string',
573
- );
574
- }
575
-
576
- function immutablePackage(manifest) {
577
- return Object.entries(manifest.immutableHashes).map(([path, hash]) => ({
578
- path,
579
- hash,
580
- }));
581
- }
582
-
583
- async function readJson(path) {
584
- return JSON.parse(await readFile(path, 'utf8'));
585
- }
586
-
587
- function joinWithin(root, relativePath) {
588
- const checked = validateSafeRelativePath(relativePath);
589
- if (!checked.valid) {
590
- throw new Error(
591
- `Unsafe durability record path: ${checked.errors[0].message}`,
592
- );
593
- }
594
- const path = resolve(root, checked.normalizedPath);
595
- assertWithin(root, path, 'Durability record');
596
- return path;
597
- }
598
-
599
- function assertWithin(root, target, label) {
600
- const path = relative(root, target);
601
- if (path === '..' || path.startsWith(`..${sep}`)) {
602
- throw new Error(`${label} escapes its configured root.`);
603
- }
604
- }
605
-
606
- function toRepoPath(repoRoot, absolutePath) {
607
- assertWithin(repoRoot, absolutePath, 'Evidence path');
608
- return relative(repoRoot, absolutePath).split(sep).join('/');
609
- }
610
-
611
- async function fileHash(path) {
612
- return bufferHash(await readFile(path));
613
- }
614
-
615
- function bufferHash(value) {
616
- return `sha256:${createHash('sha256').update(value).digest('hex')}`;
617
- }
618
-
619
- function assertValid(kind, value, context) {
620
- const result = validateContract(kind, value, context);
621
- if (!result.valid) {
622
- throw new Error(
623
- `Invalid ${kind}: ${result.errors
624
- .map(({ path, code, message }) => `${path} [${code}]: ${message}`)
625
- .join('; ')}`,
626
- );
627
- }
628
- }
629
-
630
- function arraysEqual(left, right) {
631
- return (
632
- left.length === right.length &&
633
- left.every((value, index) => value === right[index])
634
- );
635
- }
636
-
637
- function error(code, message) {
638
- return { code, message };
639
- }
640
-
641
- function errorMessage(value) {
642
- return value instanceof Error ? value.message : String(value);
643
- }