@open-agent-toolkit/cli 0.2.73 → 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 (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. 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
- }