@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,3038 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createHash } from 'node:crypto';
4
- import { lstat, readFile, realpath } from 'node:fs/promises';
5
- import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
7
-
8
- import { assertBrowserProbeSession } from './lib/browser-runtime.mjs';
9
- import {
10
- catalogFromManifest,
11
- initiativeCatalogPath,
12
- serializeInitiativeCatalog,
13
- } from './lib/catalog.mjs';
14
- import {
15
- readContentApproval,
16
- resolveContentApproval,
17
- } from './lib/content-approval.mjs';
18
- import { canonicalHash, validateContract } from './lib/contracts.mjs';
19
- import {
20
- assertAuthoredGraphSemantics,
21
- graphSemanticsForArtisticAuthor,
22
- parseDiagram,
23
- } from './lib/diagram.mjs';
24
- import { processFactBase } from './lib/fact-base.mjs';
25
- import { writeJsonAtomic, writeTextAtomic } from './lib/fs-safe.mjs';
26
- import { validateHtmlSafety } from './lib/html-safety.mjs';
27
- import { validateInternalReferences } from './lib/internal-references.mjs';
28
- import { parseMarkdown } from './lib/markdown.mjs';
29
- import { enforceRunPackageInventory } from './lib/package-coverage.mjs';
30
- import { assertManifestPublishable } from './lib/publication-policy.mjs';
31
- import {
32
- auditArtifactSet,
33
- checkGuidelines,
34
- checkSourceDumping,
35
- RENDER_QA_WARNING_IDS,
36
- renderQaWarningIds,
37
- renderWarningIds,
38
- } from './lib/qa.mjs';
39
- import {
40
- loadRecipe,
41
- recipeExpansion,
42
- recipeFloor,
43
- recipeRequiredNarrative,
44
- resolveDiagramRenderingRoute,
45
- selectRecipeAuthoring,
46
- shouldStopDiscovery,
47
- validateContentModel,
48
- validatePlannedPortfolio,
49
- validateSourceBindings,
50
- } from './lib/recipes.mjs';
51
- import {
52
- canonicalPersistedRunRequest,
53
- createSetPlanResumeToken,
54
- initializeRun,
55
- readSetPlanRecords,
56
- reopenBuildStages,
57
- supersedeTerminalEvidence,
58
- updateBuildRecord,
59
- verifySetPlanResumeToken,
60
- writeManifestAtomic,
61
- writeSetPlanRecords,
62
- writeTerminalEvidence,
63
- writeVisualReviewAttempt,
64
- writeVisualReviewFailure,
65
- writeVisualRevision,
66
- } from './lib/records.mjs';
67
- import { artifactPath, renderArtifact } from './lib/render.mjs';
68
- import { resolveRootConfinedPath } from './lib/safe-paths.mjs';
69
- import { plannedArtifacts, planExplainerSet } from './lib/set-plan.mjs';
70
- import {
71
- createVisualReviewEvidence,
72
- evidenceReason,
73
- projectThrownReason,
74
- } from './lib/terminal-evidence.mjs';
75
- import { resolveTheme } from './lib/theme.mjs';
76
- import { runVisualReview } from './lib/visual-review.mjs';
77
-
78
- export {
79
- assertBrowserProbeSession,
80
- createBrowserProbeSession,
81
- createFixtureBrowserProbeSession,
82
- } from './lib/browser-runtime.mjs';
83
-
84
- // Stages a rejected draft reruns once its content is corrected.
85
- const REOPENED_ON_REJECTION = Object.freeze(['render', 'qa']);
86
- const LOCALLY_PROJECTED_ERRORS = new WeakSet();
87
-
88
- export async function runExplainer(request, options = {}) {
89
- const normalizedRequest = normalizeRunRequest(request);
90
- assertValidRequest(normalizedRequest);
91
- const recipe = selectRecipeAuthoring(
92
- loadRecipe(normalizedRequest.recipe.id, normalizedRequest.recipe.version),
93
- normalizedRequest.recapMode,
94
- );
95
- const browserProvider = resolveBrowserProvider(
96
- normalizedRequest,
97
- recipe,
98
- options,
99
- );
100
- const resumed = await loadResumableRun(
101
- normalizedRequest,
102
- options.reviewedSource?.resumeToken,
103
- );
104
- const run = resumed ?? (await initializeRun(normalizedRequest));
105
- const now = options.now ?? (() => new Date().toISOString());
106
- const state = {
107
- run,
108
- recipe,
109
- factBase: null,
110
- factBaseHash: null,
111
- inputHashes: {},
112
- setPlan: null,
113
- setPlanPaths: [],
114
- contentModels: [],
115
- contentPaths: new Map(),
116
- authorResultPaths: [],
117
- resolvedArtifacts: [],
118
- authoredContent: new Map(),
119
- expansion: {
120
- valid: true,
121
- accepted: [],
122
- rejected: [],
123
- warnings: [],
124
- errors: [],
125
- },
126
- qaErrors: [],
127
- theme: null,
128
- themeWarnings: [],
129
- renderStrategy: run.request.theme.renderStrategy,
130
- rendered: [],
131
- artifacts: [],
132
- warnings: [],
133
- reopenedWarnings: {},
134
- discovery: { rounds: 0, findings: [], reason: 'not-requested' },
135
- approval: null,
136
- browserProvider,
137
- browserEvidence: [],
138
- visualReview: null,
139
- visualReviewPaths: [],
140
- visualReviewAttempt: 0,
141
- reviewGateBlocked: false,
142
- reviewGateReason: null,
143
- correctionAttempted: false,
144
- publishReceiptPath: null,
145
- resumeToken: null,
146
- resumedApprovalStatus: null,
147
- };
148
-
149
- try {
150
- if (resumed) {
151
- await hydrateResumableState(state);
152
- } else {
153
- validateRecipeSources(recipe, run.request.factBase);
154
- await executeStage(run, 'validate', options, async () => ({
155
- outputPaths: ['run-request.json'],
156
- }));
157
- await executeStage(run, 'fact-base', options, async () => {
158
- const processed = await buildFactBase(
159
- run.request.factBase,
160
- options,
161
- now,
162
- );
163
- state.factBase = processed.factBase;
164
- const factBaseWarnings =
165
- processed.checks.warnings.length > 0
166
- ? ['fact-base-freshness-warning']
167
- : [];
168
- state.warnings.push(...factBaseWarnings);
169
- state.inputHashes = inputHashes(processed.factBase);
170
- state.factBaseHash = canonicalHash(processed.factBase);
171
- await writeJsonAtomic(
172
- run.runRoot,
173
- 'source/fact-base.json',
174
- state.factBase,
175
- );
176
- await writeTextAtomic(
177
- run.runRoot,
178
- 'source/fact-base.md',
179
- factBaseMarkdown(state.factBase),
180
- );
181
- return {
182
- outputPaths: ['source/fact-base.json', 'source/fact-base.md'],
183
- warnings: factBaseWarnings,
184
- status: factBaseWarnings.length > 0 ? 'warned' : 'passed',
185
- };
186
- });
187
- await prepareTheme(state);
188
- await executeStage(run, 'content', options, async () => {
189
- state.discovery = await runDiscovery(recipe, state.factBase, options);
190
- const planned = await planExplainerSet({
191
- recipe,
192
- factBase: state.factBase,
193
- discovery: state.discovery,
194
- planSet: options.planSet,
195
- });
196
- state.setPlan = planned.plan;
197
- const portfolioValidation = validatePlannedPortfolio(
198
- recipe,
199
- state.setPlan.portfolio,
200
- );
201
- if (!portfolioValidation.valid) {
202
- throw codedError(
203
- 'E_SET_PLAN',
204
- `Invalid planned portfolio: ${portfolioValidation.errors.join('; ')}`,
205
- );
206
- }
207
- state.setPlanPaths = await writeSetPlanRecords(run, planned);
208
- await createAuthoredContent(state, options, now);
209
- return {
210
- outputPaths: [
211
- ...state.setPlanPaths,
212
- ...state.contentPaths.values(),
213
- ...state.authorResultPaths,
214
- ],
215
- warnings: state.expansion.warnings,
216
- status: state.expansion.warnings.length > 0 ? 'warned' : 'passed',
217
- };
218
- });
219
- }
220
-
221
- if (!resumed) {
222
- await executeThemeStage(state, options);
223
- }
224
- if (!resumed || state.resumedApprovalStatus === 'rejected') {
225
- if (state.resumedApprovalStatus === 'rejected') {
226
- const reopened = await reopenBuildStages(run, {
227
- ids: [...REOPENED_ON_REJECTION],
228
- reason: 'content-rejected',
229
- });
230
- // Reopen markers are the D4 audit trail, so they survive the rerun's
231
- // replacement of each stage's warning set.
232
- state.reopenedWarnings = Object.fromEntries(
233
- reopened.stages.map(({ id, warnings }) => [
234
- id,
235
- warnings.filter((warning) => warning.startsWith('stage-reopened:')),
236
- ]),
237
- );
238
- }
239
- await executeRenderStage(state, options);
240
- await executeQaStage(state, options, now);
241
- }
242
-
243
- state.approval = await resolveContentApproval(
244
- run,
245
- run.request.mode,
246
- options.reviewedSource,
247
- state.authorResultPaths,
248
- approvalArtifacts(state),
249
- );
250
- if (!state.approval.canResume) {
251
- if (run.request.mode === 'interactive') {
252
- state.resumeToken = await createSetPlanResumeToken(run);
253
- }
254
- const manifest = await inventoryManifestFor(state, now());
255
- await enforceRetainedRunPackage(state, manifest, {
256
- includeManifest: false,
257
- failureStage: 'qa',
258
- });
259
- return resultFor(state);
260
- }
261
-
262
- if (state.reviewGateBlocked) {
263
- await updateBuildRecord(run, { id: 'durability', status: 'skipped' });
264
- await updateBuildRecord(run, { id: 'publish', status: 'skipped' });
265
- const manifest = await persistManifest(state, now());
266
- await enforceRetainedRunPackage(state, manifest, {
267
- acceptCleanedUnexpected: true,
268
- });
269
- await writeTerminalEvidence(run, {
270
- outcome: 'built-needs-review',
271
- manifest,
272
- reasons: terminalReasonsForReview(state),
273
- evidenceDisposition: state.visualReview ? 'retained' : 'partial',
274
- });
275
- return resultFor(state);
276
- }
277
-
278
- const manifestFinalized = await executeDurabilityAndPublish(
279
- state,
280
- options,
281
- now,
282
- );
283
- if (!manifestFinalized) {
284
- const manifest = await persistManifest(state, now());
285
- await enforceRetainedRunPackage(state, manifest, {
286
- failureStage: 'qa',
287
- });
288
- } else {
289
- const manifest = JSON.parse(
290
- await readFile(state.run.manifestPath, 'utf8'),
291
- );
292
- await enforceRetainedRunPackage(state, manifest, {
293
- failureStage: 'qa',
294
- });
295
- }
296
- return resultFor(state);
297
- } catch (error) {
298
- let manifest;
299
- if (state.theme && state.factBase) {
300
- await persistFailureManifest(state, error, now()).catch(() => {});
301
- manifest = await readFile(state.run.manifestPath, 'utf8')
302
- .then(JSON.parse)
303
- .catch(() => undefined);
304
- }
305
- const reason =
306
- localEvidenceReason(error) ??
307
- projectThrownReason('finalization', 'pipeline-failure');
308
- // The inventory verdict is reached *before* terminal evidence is written,
309
- // so the single write below carries the complete reason set. Terminal
310
- // evidence is immutable once retained (`records.mjs` refuses a second
311
- // write unless the path is absent), so a re-record after the fact could
312
- // never succeed; the previous shape swallowed that guaranteed failure in a
313
- // `.catch(() => {})` under a comment claiming the durable evidence was
314
- // updated. `includeTerminalEvidence` is correspondingly dropped here,
315
- // because the file legitimately does not exist yet at this point.
316
- let reasons = [reason];
317
- if (manifest) {
318
- try {
319
- await enforceRetainedRunPackage(state, manifest);
320
- } catch (inventoryError) {
321
- // This used to be swallowed outright. It must not simply be reported
322
- // either: a failed run routinely leaves partial outputs, and the first
323
- // pass removes them and *then* throws, so the common case is a
324
- // successful repair rather than a violated invariant. Re-running
325
- // separates the two — if the tree is clean now, the repair worked and
326
- // there is nothing to report; if it still fails, the violation is
327
- // genuinely unremovable or a required file is missing, which is the
328
- // case that previously went unreported at run time.
329
- const repaired = await enforceRetainedRunPackage(state, manifest).then(
330
- () => true,
331
- () => false,
332
- );
333
- if (!repaired) {
334
- reasons = mergeTerminalReasons(
335
- reasons,
336
- localEvidenceReason(inventoryError) ??
337
- projectThrownReason('finalization', 'pipeline-failure'),
338
- );
339
- }
340
- }
341
- }
342
- await writeTerminalEvidence(run, {
343
- outcome: 'failed',
344
- manifest,
345
- reasons,
346
- evidenceDisposition: manifest ? 'retained' : 'unavailable',
347
- });
348
- return resultFor(state, { failed: true, reasons });
349
- }
350
- }
351
-
352
- export async function supersedeExplainerRun({ runRoot, supersededBy } = {}) {
353
- if (typeof runRoot !== 'string') {
354
- throw new TypeError('Supersession requires a runRoot.');
355
- }
356
- const confinedRunRoot = await realpath(runRoot);
357
- const manifest = JSON.parse(
358
- await readFile(join(confinedRunRoot, 'manifest.json'), 'utf8'),
359
- );
360
- if (
361
- manifest?.schemaVersion !== 'explainer-kit.manifest/v1' ||
362
- typeof manifest.runId !== 'string' ||
363
- typeof manifest.slug !== 'string' ||
364
- !['built-needs-review', 'failed'].includes(manifest.outcome)
365
- ) {
366
- throw new Error(
367
- 'Only a flagged or failed manifest can produce supersession evidence.',
368
- );
369
- }
370
- if (
371
- !supersededBy ||
372
- typeof supersededBy !== 'object' ||
373
- typeof supersededBy.runId !== 'string' ||
374
- supersededBy.runId === manifest.runId ||
375
- !/^sha256:[a-f0-9]{64}$/.test(supersededBy.manifestHash ?? '')
376
- ) {
377
- throw new Error(
378
- 'Supersession requires a distinct replacement run ID and manifest hash.',
379
- );
380
- }
381
- await supersedeTerminalEvidence(
382
- {
383
- runId: manifest.runId,
384
- slug: manifest.slug,
385
- runRoot: confinedRunRoot,
386
- },
387
- { manifest, supersededBy },
388
- );
389
- return {
390
- runId: manifest.runId,
391
- outcome: manifest.outcome,
392
- terminalEvidencePath: join(confinedRunRoot, 'terminal-evidence.json'),
393
- supersededBy: structuredClone(supersededBy),
394
- };
395
- }
396
-
397
- async function executeThemeStage(state, options) {
398
- await executeStage(state.run, 'theme', options, async () => {
399
- await writeJsonAtomic(
400
- state.run.runRoot,
401
- 'theme.resolved.json',
402
- state.theme,
403
- );
404
- return {
405
- outputPaths: ['theme.resolved.json'],
406
- warnings: state.themeWarnings,
407
- status: state.themeWarnings.length > 0 ? 'warned' : 'passed',
408
- };
409
- });
410
- }
411
-
412
- async function prepareTheme(state) {
413
- const resolved = await resolveTheme(state.run.request.theme);
414
- state.theme = resolved.theme;
415
- state.renderStrategy = resolved.renderStrategy;
416
- state.themeWarnings =
417
- resolved.warnings.length > 0 ? ['theme-selection-normalized'] : [];
418
- state.warnings.push(...state.themeWarnings);
419
- }
420
-
421
- async function executeRenderStage(state, options) {
422
- state.rendered = [];
423
- state.artifacts = [];
424
- await executeStage(state.run, 'render', options, async () => {
425
- for (const artifact of state.resolvedArtifacts) {
426
- const rendered =
427
- artifact.authoring === 'markdown'
428
- ? await renderArtifact({
429
- recipeArtifact: renderDescriptor(artifact),
430
- content: state.contentModels.find(
431
- ({ artifactId }) => artifactId === artifact.id,
432
- ),
433
- factBase: state.factBase,
434
- theme: state.theme,
435
- renderStrategy: state.renderStrategy,
436
- ...(state.run.request.publicBaseUrl && {
437
- publicBaseUrl: state.run.request.publicBaseUrl,
438
- }),
439
- })
440
- : artisticRender(state, artifact);
441
- await writeTextAtomic(
442
- state.run.runRoot,
443
- rendered.renderedPath,
444
- rendered.html,
445
- );
446
- state.rendered.push(rendered);
447
- state.artifacts.push(artifactRecord(state, rendered));
448
- }
449
- // D7 degradation findings are guideline severity, so they travel to the
450
- // run result and the manifest as warnings rather than failing the stage.
451
- const degradation = renderWarningIds(
452
- state.rendered.flatMap(
453
- ({ warnings: sectionWarnings = [] }) => sectionWarnings,
454
- ),
455
- );
456
- state.warnings.push(...degradation);
457
- const warnings = [...(state.reopenedWarnings.render ?? []), ...degradation];
458
- return {
459
- outputPaths: state.rendered.map(({ renderedPath }) => renderedPath),
460
- warnings,
461
- status: warnings.length > 0 ? 'warned' : 'passed',
462
- };
463
- });
464
- }
465
-
466
- async function executeQaStage(state, options, now) {
467
- await executeStage(state.run, 'qa', options, async () => {
468
- const htmlSafetyErrors = [];
469
- const qaWarnings = [];
470
- try {
471
- return await auditRenderedArtifacts(state, options, now, {
472
- browserProvider: state.browserProvider,
473
- htmlSafetyErrors,
474
- qaWarnings,
475
- });
476
- } catch (error) {
477
- const reviewError = normalizeReviewGateError(state, error);
478
- if (!reviewError) {
479
- throw error;
480
- }
481
- const warning = reviewGateWarning(reviewError);
482
- state.reviewGateBlocked = true;
483
- state.reviewGateReason =
484
- localEvidenceReason(reviewError) ??
485
- evidenceReason('browser-review', 'pipeline-failure');
486
- state.warnings.push(warning);
487
- return {
488
- outputPaths: [
489
- ...state.rendered.map(({ renderedPath }) => renderedPath),
490
- ...state.visualReviewPaths,
491
- ],
492
- warnings: [warning],
493
- status: 'warned',
494
- };
495
- }
496
- });
497
- }
498
-
499
- /**
500
- * Render QA drives a caller-supplied session only. The core never launches a
501
- * headless runtime itself. Production recap evidence requires a session whose
502
- * identity was derived and branded by createBrowserProbeSession().
503
- */
504
- function resolveBrowserProvider(request, recipe, options) {
505
- if (
506
- options.browserSession !== undefined &&
507
- options.browserProbe !== undefined
508
- ) {
509
- throw codedError(
510
- 'E_BROWSER_PROBE',
511
- 'Supply either options.browserSession or the legacy non-retaining options.browserProbe callback, not both.',
512
- );
513
- }
514
- const productionRecap =
515
- recipe.id === 'project-recap' && request.mode === 'unattended';
516
- if (options.browserSession !== undefined) {
517
- try {
518
- return {
519
- session: assertBrowserProbeSession(options.browserSession, {
520
- allowFixture: true,
521
- }),
522
- probe: null,
523
- productionRecap,
524
- };
525
- } catch (error) {
526
- throw codedError(
527
- 'E_BROWSER_PROBE',
528
- error?.message ?? 'Browser session validation failed.',
529
- );
530
- }
531
- }
532
- if (options.browserProbe === undefined) return null;
533
- if (productionRecap) {
534
- throw codedError(
535
- 'E_BROWSER_PROBE',
536
- 'Unattended project recaps require a trusted launched-Chromium browserSession; bare callbacks and caller-asserted runtime metadata are not accepted.',
537
- );
538
- }
539
- if (typeof options.browserProbe !== 'function') {
540
- throw codedError(
541
- 'E_BROWSER_PROBE',
542
- 'options.browserProbe must be a function when supplied.',
543
- );
544
- }
545
- return {
546
- session: null,
547
- probe: async (...args) => {
548
- try {
549
- return await options.browserProbe(...args);
550
- } catch {
551
- throw withEvidenceReason(
552
- codedError('E_VISUAL_REVIEW', 'Browser evidence callback failed.'),
553
- 'browser-review',
554
- 'provider-failure',
555
- );
556
- }
557
- },
558
- productionRecap,
559
- };
560
- }
561
-
562
- async function auditRenderedArtifacts(
563
- state,
564
- options,
565
- now,
566
- { browserProvider, htmlSafetyErrors, qaWarnings },
567
- ) {
568
- await enforceInternalReferenceGate(state, options, now);
569
- for (const artifact of state.resolvedArtifacts.filter(
570
- ({ authoring }) => authoring === 'html',
571
- )) {
572
- const safety = validateHtmlSafety({
573
- html: state.authoredContent.get(artifact.id),
574
- shell: artifact.shellContent,
575
- shellName: artifact.shell ?? artifact.template,
576
- });
577
- htmlSafetyErrors.push(
578
- ...safety.errors.map((code) => ({
579
- code,
580
- message: `Artistic artifact ${artifact.id} failed DOM safety validation.`,
581
- })),
582
- );
583
- qaWarnings.push(...safety.warnings);
584
- }
585
-
586
- const probeArtifacts = state.rendered.map((artifact) => ({
587
- id: artifact.artifactId,
588
- type: artifact.type,
589
- html: artifact.html,
590
- }));
591
- let report;
592
- try {
593
- report = await auditArtifactSet({
594
- artifacts: probeArtifacts,
595
- setPlan: state.setPlan,
596
- ...(options.denylist && { denylist: options.denylist }),
597
- ...(browserProvider?.session && {
598
- browserSession: browserProvider.session,
599
- }),
600
- ...(browserProvider?.probe && { browserProbe: browserProvider.probe }),
601
- ...(options.widths &&
602
- !requiresRecapVisualReview(state) && { widths: options.widths }),
603
- ...(browserProvider &&
604
- requiresRecapVisualReview(state) && {
605
- evidenceRoot: state.run.runRoot,
606
- requireBrowserEvidence: true,
607
- }),
608
- });
609
- } catch (error) {
610
- if (requiresRecapVisualReview(state)) {
611
- throw withEvidenceReason(
612
- codedError('E_VISUAL_REVIEW', 'Browser review failed.'),
613
- 'browser-review',
614
- 'provider-failure',
615
- );
616
- }
617
- throw error;
618
- }
619
- const hardIssues = report.issues.filter((issue) => isHardQaIssue(issue.code));
620
- const warningIssues = report.issues.filter(
621
- (issue) => !isHardQaIssue(issue.code),
622
- );
623
- // A code the render-QA vocabulary already covers must not also emit an ad
624
- // hoc `qa-*` twin; the generic conversion is for structural codes only.
625
- qaWarnings.push(
626
- ...renderQaWarningIds(warningIssues),
627
- ...warningIssues
628
- .filter(({ code }) => renderQaWarningIds([{ code }]).length === 0)
629
- .map(({ code }) => `qa-${code}`),
630
- );
631
- if (!browserProvider) {
632
- qaWarnings.push(RENDER_QA_WARNING_IDS.skippedNoProbe);
633
- }
634
- const guidelines = checkGuidelines({
635
- recipe: state.recipe,
636
- artifacts: probeArtifacts,
637
- expansion: state.expansion,
638
- });
639
- qaWarnings.push(...guidelines.warnings, ...state.expansion.warnings);
640
-
641
- const errors = [...state.qaErrors, ...htmlSafetyErrors, ...hardIssues];
642
- if (errors.length > 0) {
643
- throw codedError(
644
- 'E_QA',
645
- errors.map(({ code, message }) => `${code}: ${message}`).join('; '),
646
- );
647
- }
648
- state.browserEvidence = report.browser?.evidence ?? [];
649
- const visualCritic = resolveVisualCritic(options);
650
- const reviewRequired = requiresRecapVisualReview(state);
651
- const fixtureBrowserSession =
652
- browserProvider?.session?.runtime.kind === 'fixture';
653
- if (reviewRequired && !browserProvider) {
654
- qaWarnings.push('visual-review-required:browser-probe-missing');
655
- } else if (reviewRequired && !visualCritic) {
656
- qaWarnings.push('visual-review-required:visual-critic-missing');
657
- } else if (visualCritic) {
658
- await reviewAndRetain(state, visualCritic, 1);
659
- if (state.visualReview.result.disposition === 'correct') {
660
- if (state.correctionAttempted) {
661
- throw codedError(
662
- 'E_VISUAL_CORRECTION',
663
- 'The one bounded artifact correction was already consumed by internal-reference validation.',
664
- );
665
- }
666
- state.correctionAttempted = true;
667
- await applyVisualCorrection(state, options, now);
668
- const correctedReferences = validateRenderedInternalReferences(state);
669
- if (!correctedReferences.valid) {
670
- throw internalReferenceError(correctedReferences.errors);
671
- }
672
- const correctedArtifacts = state.rendered.map((artifact) => ({
673
- id: artifact.artifactId,
674
- type: artifact.type,
675
- html: artifact.html,
676
- }));
677
- let finalReport;
678
- try {
679
- finalReport = await auditArtifactSet({
680
- artifacts: correctedArtifacts,
681
- setPlan: state.setPlan,
682
- ...(options.denylist && { denylist: options.denylist }),
683
- ...(browserProvider?.session && {
684
- browserSession: browserProvider.session,
685
- }),
686
- ...(browserProvider?.probe && {
687
- browserProbe: browserProvider.probe,
688
- }),
689
- ...(options.widths &&
690
- !requiresRecapVisualReview(state) && { widths: options.widths }),
691
- ...(browserProvider &&
692
- requiresRecapVisualReview(state) && {
693
- evidenceRoot: state.run.runRoot,
694
- requireBrowserEvidence: true,
695
- }),
696
- });
697
- } catch {
698
- throw withEvidenceReason(
699
- codedError('E_VISUAL_REVIEW', 'Corrected browser review failed.'),
700
- 'browser-review',
701
- 'provider-failure',
702
- );
703
- }
704
- const finalHardIssues = finalReport.issues.filter((issue) =>
705
- isHardQaIssue(issue.code),
706
- );
707
- if (finalHardIssues.length > 0) {
708
- throw codedError(
709
- 'E_VISUAL_CORRECTION',
710
- finalHardIssues
711
- .map(({ code, message }) => `${code}: ${message}`)
712
- .join('; '),
713
- );
714
- }
715
- const finalWarnings = finalReport.issues.filter(
716
- (issue) => !isHardQaIssue(issue.code),
717
- );
718
- qaWarnings.push(
719
- ...renderQaWarningIds(finalWarnings),
720
- ...finalWarnings
721
- .filter(({ code }) => renderQaWarningIds([{ code }]).length === 0)
722
- .map(({ code }) => `qa-${code}`),
723
- );
724
- state.browserEvidence = finalReport.browser?.evidence ?? [];
725
- await reviewAndRetain(state, visualCritic, 2);
726
- }
727
- }
728
- if (reviewRequired && fixtureBrowserSession) {
729
- qaWarnings.push('visual-review-required:fixture-browser-session');
730
- }
731
- if (
732
- reviewRequired &&
733
- state.visualReview &&
734
- state.visualReview.result.disposition !== 'pass'
735
- ) {
736
- qaWarnings.push(
737
- state.visualReview.result.disposition === 'fail'
738
- ? 'visual-review-required:critic-failed'
739
- : 'visual-review-required:correction-unresolved',
740
- );
741
- }
742
- state.reviewGateBlocked = qaWarnings.some((warning) =>
743
- warning.startsWith('visual-review-required:'),
744
- );
745
- state.warnings.push(...qaWarnings);
746
- const warnings = [
747
- ...(state.reopenedWarnings.qa ?? []),
748
- ...new Set(qaWarnings),
749
- ];
750
- return {
751
- outputPaths: [
752
- ...state.rendered.map(({ renderedPath }) => renderedPath),
753
- ...state.visualReviewPaths,
754
- ],
755
- warnings,
756
- status: warnings.length > 0 ? 'warned' : 'passed',
757
- };
758
- }
759
-
760
- async function enforceInternalReferenceGate(state, options, now) {
761
- const validate = () => validateRenderedInternalReferences(state);
762
- const initial = validate();
763
- if (initial.valid) return;
764
-
765
- const correctionAuthor = options.correctArtifact ?? options.author;
766
- if (typeof correctionAuthor !== 'function') {
767
- throw internalReferenceError(initial.errors);
768
- }
769
- const trust = authorTrustContext(options, now);
770
- const artifactIds = [
771
- ...new Set(
772
- initial.errors
773
- .map(({ artifactId }) => artifactId)
774
- .filter((artifactId) => typeof artifactId === 'string'),
775
- ),
776
- ];
777
- if (artifactIds.length === 0) {
778
- throw internalReferenceError(initial.errors);
779
- }
780
- state.correctionAttempted = true;
781
- for (const artifactId of artifactIds) {
782
- const artifactIndex = state.resolvedArtifacts.findIndex(
783
- ({ id }) => id === artifactId,
784
- );
785
- if (artifactIndex < 0) {
786
- throw internalReferenceError(initial.errors);
787
- }
788
- const artifact = state.resolvedArtifacts[artifactIndex];
789
- let item;
790
- try {
791
- item = await authorArtifact(
792
- state,
793
- artifact,
794
- correctionAuthor,
795
- trust,
796
- canonicalArtifactLinks(
797
- state.resolvedArtifacts,
798
- artifact.id,
799
- state.run.slug,
800
- ),
801
- {
802
- attempt: 1,
803
- reason: 'internal-reference-validation',
804
- findings: structuredClone(
805
- initial.errors.filter(
806
- ({ artifactId: findingArtifactId }) =>
807
- findingArtifactId === artifactId,
808
- ),
809
- ),
810
- previousContentPath: state.contentPaths.get(artifactId),
811
- },
812
- );
813
- } catch (error) {
814
- throw withEvidenceReason(
815
- codedError(
816
- 'E_INTERNAL_REFERENCE',
817
- `Internal-reference correction failed for ${artifactId}: ${safeMessage(error)}`,
818
- ),
819
- 'link-validation',
820
- 'pipeline-failure',
821
- );
822
- }
823
- await installCorrectedArtifact(state, artifactIndex, item);
824
- }
825
-
826
- const final = validate();
827
- if (!final.valid) {
828
- throw internalReferenceError(final.errors);
829
- }
830
- }
831
-
832
- function validateRenderedInternalReferences(state) {
833
- return validateInternalReferences({
834
- artifacts: state.rendered.map((artifact) => ({
835
- artifactId: artifact.artifactId,
836
- renderedPath: artifact.renderedPath,
837
- html: artifact.html,
838
- })),
839
- manifestPaths: state.artifacts.map(({ renderedPath }) => renderedPath),
840
- });
841
- }
842
-
843
- async function installCorrectedArtifact(state, artifactIndex, item) {
844
- const artifactId = item.artifact.id;
845
- await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
846
- await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
847
- state.resolvedArtifacts[artifactIndex] = item.artifact;
848
- state.authoredContent.set(artifactId, item.content);
849
- state.contentPaths.set(artifactId, item.contentPath);
850
- if (item.artifact.authoring === 'markdown') {
851
- const links = expansionLinks(state.resolvedArtifacts);
852
- const model = assertValidContentModel(
853
- state.recipe,
854
- markdownContentModel(
855
- item.artifact,
856
- state.run.slug,
857
- item.content,
858
- item.artifact.origin === 'floor' ? links : [],
859
- ),
860
- item.artifact,
861
- );
862
- const modelIndex = state.contentModels.findIndex(
863
- ({ artifactId: id }) => id === artifactId,
864
- );
865
- state.contentModels[modelIndex] = model;
866
- }
867
- const rendered =
868
- item.artifact.authoring === 'markdown'
869
- ? await renderArtifact({
870
- recipeArtifact: renderDescriptor(item.artifact),
871
- content: state.contentModels.find(
872
- ({ artifactId: id }) => id === artifactId,
873
- ),
874
- factBase: state.factBase,
875
- theme: state.theme,
876
- renderStrategy: state.renderStrategy,
877
- ...(state.run.request.publicBaseUrl && {
878
- publicBaseUrl: state.run.request.publicBaseUrl,
879
- }),
880
- })
881
- : artisticRender(state, item.artifact);
882
- await writeTextAtomic(
883
- state.run.runRoot,
884
- rendered.renderedPath,
885
- rendered.html,
886
- );
887
- const renderedIndex = state.rendered.findIndex(
888
- ({ artifactId: id }) => id === artifactId,
889
- );
890
- state.rendered[renderedIndex] = rendered;
891
- state.artifacts[renderedIndex] = artifactRecord(state, rendered);
892
- }
893
-
894
- function internalReferenceError(errors) {
895
- // `link-validation` is one of the eight closed evidence stages and is
896
- // accepted by both evidence schemas, but the gate runs inside the `qa` build
897
- // stage, and `evidenceStageForBuildStage` maps `qa -> browser-review`. Without
898
- // an explicit local reason a broken internal link is durably recorded as a
899
- // browser-review failure, which is the one thing the closed stage enum exists
900
- // to prevent. Fail-closed behavior is unchanged; only attribution improves.
901
- return withEvidenceReason(
902
- codedError(
903
- 'E_INTERNAL_REFERENCE',
904
- errors
905
- .map(
906
- ({ code, renderedPath, reference, message }) =>
907
- `${code}: ${renderedPath ?? 'site'}${reference ? ` references ${reference}` : ''}: ${message}`,
908
- )
909
- .join('; '),
910
- ),
911
- 'link-validation',
912
- 'pipeline-failure',
913
- );
914
- }
915
-
916
- function resolveVisualCritic(options) {
917
- if (options.visualCritic === undefined) return null;
918
- if (typeof options.visualCritic !== 'function') {
919
- throw codedError(
920
- 'E_VISUAL_REVIEW',
921
- 'options.visualCritic must be a function when supplied.',
922
- );
923
- }
924
- if (
925
- options.visualCritic === options.author ||
926
- options.visualCritic === options.critic
927
- ) {
928
- throw codedError(
929
- 'E_VISUAL_REVIEW',
930
- 'The visual critic must be distinct from the artifact author and fact critic.',
931
- );
932
- }
933
- return options.visualCritic;
934
- }
935
-
936
- function requiresRecapVisualReview(state) {
937
- return (
938
- state.recipe.id === 'project-recap' &&
939
- state.run.request.mode === 'unattended'
940
- );
941
- }
942
-
943
- async function reviewAndRetain(state, visualCritic, attempt) {
944
- state.visualReviewAttempt = attempt;
945
- try {
946
- const review = await runVisualReview({
947
- plan: state.setPlan,
948
- rendered: state.rendered,
949
- evidence: state.browserEvidence,
950
- visualCritic,
951
- runRoot: state.run.runRoot,
952
- });
953
- state.visualReview = {
954
- ...review,
955
- evidence: createVisualReviewEvidence({
956
- request: review.request,
957
- attempt,
958
- result: review.result,
959
- }),
960
- };
961
- state.visualReviewPaths.push(
962
- ...(await writeVisualReviewAttempt(state.run, {
963
- attempt,
964
- review: state.visualReview,
965
- })),
966
- );
967
- } catch (error) {
968
- if (error?.visualReviewRequest) {
969
- const kind = ['provider-failure', 'pipeline-failure'].includes(
970
- error.evidenceKind,
971
- )
972
- ? error.evidenceKind
973
- : 'pipeline-failure';
974
- const evidence = createVisualReviewEvidence({
975
- request: error.visualReviewRequest,
976
- attempt,
977
- failureKind: kind,
978
- });
979
- state.visualReview = {
980
- request: structuredClone(error.visualReviewRequest),
981
- evidence,
982
- };
983
- state.visualReviewPaths.push(
984
- ...(await writeVisualReviewFailure(state.run, {
985
- attempt,
986
- request: error.visualReviewRequest,
987
- kind,
988
- })),
989
- );
990
- throw withEvidenceReason(
991
- codedError('E_VISUAL_REVIEW', 'Visual review failed.'),
992
- 'visual-review',
993
- kind,
994
- );
995
- }
996
- throw withEvidenceReason(
997
- codedError('E_VISUAL_REVIEW', 'Visual review pipeline failed.'),
998
- 'visual-review',
999
- 'pipeline-failure',
1000
- );
1001
- }
1002
- }
1003
-
1004
- async function applyVisualCorrection(state, options, now) {
1005
- const findings = state.visualReview.result.findings;
1006
- const artifactIds = [
1007
- ...new Set(findings.map(({ artifactId }) => artifactId)),
1008
- ];
1009
- const correctionAuthor = options.correctArtifact ?? options.author;
1010
- if (typeof correctionAuthor !== 'function') {
1011
- throw codedError(
1012
- 'E_VISUAL_CORRECTION',
1013
- 'A correct disposition requires an artifact correction callback.',
1014
- );
1015
- }
1016
- const trust = authorTrustContext(options, now);
1017
- const changes = [];
1018
- for (const artifactId of artifactIds) {
1019
- const artifactIndex = state.resolvedArtifacts.findIndex(
1020
- ({ id }) => id === artifactId,
1021
- );
1022
- if (artifactIndex < 0) {
1023
- throw codedError(
1024
- 'E_VISUAL_CORRECTION',
1025
- `Visual correction references unknown artifact ${artifactId}.`,
1026
- );
1027
- }
1028
- const artifact = state.resolvedArtifacts[artifactIndex];
1029
- const previousContent = state.authoredContent.get(artifactId);
1030
- let item;
1031
- try {
1032
- item = await authorArtifact(
1033
- state,
1034
- artifact,
1035
- correctionAuthor,
1036
- trust,
1037
- canonicalArtifactLinks(
1038
- state.resolvedArtifacts,
1039
- artifact.id,
1040
- state.run.slug,
1041
- ),
1042
- {
1043
- attempt: 1,
1044
- findings: structuredClone(
1045
- findings.filter((finding) => finding.artifactId === artifactId),
1046
- ),
1047
- previousContentPath: state.contentPaths.get(artifactId),
1048
- },
1049
- );
1050
- } catch {
1051
- throw withEvidenceReason(
1052
- codedError('E_VISUAL_CORRECTION', 'Visual correction provider failed.'),
1053
- 'visual-review',
1054
- 'provider-failure',
1055
- );
1056
- }
1057
- if ((item.result.proposedArtifacts ?? []).length > 0) {
1058
- throw codedError(
1059
- 'E_VISUAL_CORRECTION',
1060
- 'Visual correction cannot change the validated artifact portfolio.',
1061
- );
1062
- }
1063
- if (item.artifact.authoring === 'html') {
1064
- const safety = validateHtmlSafety({
1065
- html: item.content,
1066
- shell: item.artifact.shellContent,
1067
- shellName: item.artifact.shell ?? item.artifact.template,
1068
- });
1069
- if (!safety.valid) {
1070
- throw codedError(
1071
- 'E_QA',
1072
- `Corrected artifact ${artifactId} failed DOM safety validation.`,
1073
- );
1074
- }
1075
- }
1076
- await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
1077
- await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
1078
- state.resolvedArtifacts[artifactIndex] = item.artifact;
1079
- state.authoredContent.set(artifactId, item.content);
1080
- state.contentPaths.set(artifactId, item.contentPath);
1081
- if (item.artifact.authoring === 'markdown') {
1082
- const links = expansionLinks(state.resolvedArtifacts);
1083
- const model = assertValidContentModel(
1084
- state.recipe,
1085
- markdownContentModel(
1086
- item.artifact,
1087
- state.run.slug,
1088
- item.content,
1089
- item.artifact.origin === 'floor' ? links : [],
1090
- ),
1091
- item.artifact,
1092
- );
1093
- const modelIndex = state.contentModels.findIndex(
1094
- ({ artifactId: id }) => id === artifactId,
1095
- );
1096
- state.contentModels[modelIndex] = model;
1097
- }
1098
- const rendered =
1099
- item.artifact.authoring === 'markdown'
1100
- ? await renderArtifact({
1101
- recipeArtifact: renderDescriptor(item.artifact),
1102
- content: state.contentModels.find(
1103
- ({ artifactId: id }) => id === artifactId,
1104
- ),
1105
- factBase: state.factBase,
1106
- theme: state.theme,
1107
- renderStrategy: state.renderStrategy,
1108
- ...(state.run.request.publicBaseUrl && {
1109
- publicBaseUrl: state.run.request.publicBaseUrl,
1110
- }),
1111
- })
1112
- : artisticRender(state, item.artifact);
1113
- await writeTextAtomic(
1114
- state.run.runRoot,
1115
- rendered.renderedPath,
1116
- rendered.html,
1117
- );
1118
- const renderedIndex = state.rendered.findIndex(
1119
- ({ artifactId: id }) => id === artifactId,
1120
- );
1121
- state.rendered[renderedIndex] = rendered;
1122
- state.artifacts[renderedIndex] = artifactRecord(state, rendered);
1123
- changes.push({
1124
- artifactId,
1125
- contentPath: item.contentPath,
1126
- authorResultPath: item.resultPath,
1127
- previousHash: hashBytes(previousContent),
1128
- revisedHash: hashBytes(item.content),
1129
- });
1130
- }
1131
- state.visualReviewPaths.push(
1132
- ...(await writeVisualRevision(state.run, { artifactIds, changes })),
1133
- );
1134
- }
1135
-
1136
- function isHardQaIssue(code) {
1137
- return (
1138
- [
1139
- 'denylisted-string',
1140
- 'external-asset',
1141
- 'link-form',
1142
- 'tag-balance',
1143
- 'unresolved-token',
1144
- ].includes(code) || code.startsWith('cohesion-')
1145
- );
1146
- }
1147
-
1148
- function isReviewGateError(error) {
1149
- return ['E_VISUAL_REVIEW', 'E_VISUAL_CORRECTION'].includes(error?.code);
1150
- }
1151
-
1152
- function normalizeReviewGateError(state, error) {
1153
- if (!requiresRecapVisualReview(state)) return null;
1154
- if (isReviewGateError(error)) {
1155
- if (localEvidenceReason(error)) return error;
1156
- return withEvidenceReason(error, 'visual-review', 'pipeline-failure');
1157
- }
1158
- return null;
1159
- }
1160
-
1161
- function reviewGateWarning(error) {
1162
- const reason =
1163
- error?.code === 'E_VISUAL_CORRECTION'
1164
- ? 'correction-failed'
1165
- : localEvidenceReason(error)?.stage === 'browser-review'
1166
- ? 'browser-review-failed'
1167
- : 'review-chain-failed';
1168
- return `visual-review-required:${reason}`;
1169
- }
1170
-
1171
- function artisticRender(state, artifact) {
1172
- const renderedPath = artifactPath(renderDescriptor(artifact), state.run.slug);
1173
- const publicBaseUrl = state.run.request.publicBaseUrl?.replace(/\/+$/g, '');
1174
- return {
1175
- artifactId: artifact.id,
1176
- type: artifact.type,
1177
- renderedPath,
1178
- publicUrl: publicBaseUrl
1179
- ? `${publicBaseUrl}/${renderedPath.slice('site/'.length)}`
1180
- : undefined,
1181
- mediaType: 'text/html',
1182
- html: state.authoredContent.get(artifact.id),
1183
- warnings: [],
1184
- };
1185
- }
1186
-
1187
- function normalizeRunRequest(request) {
1188
- const normalized = structuredClone(request);
1189
- if (
1190
- normalized.recipe?.id === 'project-recap' &&
1191
- normalized.recapMode === undefined
1192
- ) {
1193
- normalized.recapMode = 'artistic';
1194
- }
1195
- return normalized;
1196
- }
1197
-
1198
- async function loadResumableRun(request, resumeToken) {
1199
- const normalized = structuredClone(request);
1200
- let canonicalOutputRoot;
1201
- try {
1202
- canonicalOutputRoot = await realpath(resolve(normalized.outputRoot));
1203
- } catch (error) {
1204
- if (error?.code === 'ENOENT') return null;
1205
- throw error;
1206
- }
1207
- const runRoot = join(canonicalOutputRoot, normalized.slug);
1208
- let runRootStats;
1209
- try {
1210
- runRootStats = await lstat(runRoot);
1211
- } catch (error) {
1212
- if (error?.code === 'ENOENT') return null;
1213
- throw error;
1214
- }
1215
- if (runRootStats.isSymbolicLink() || !runRootStats.isDirectory()) {
1216
- throw codedError(
1217
- 'E_APPROVAL_RESUME',
1218
- 'The resumable run root must be a real directory, not a symbolic link.',
1219
- );
1220
- }
1221
- const canonicalRunRoot = await realpath(runRoot);
1222
- if (
1223
- canonicalRunRoot !== runRoot ||
1224
- !isStrictDescendant(canonicalOutputRoot, canonicalRunRoot)
1225
- ) {
1226
- throw codedError(
1227
- 'E_APPROVAL_RESUME',
1228
- 'The resumable run root escapes the configured output root.',
1229
- );
1230
- }
1231
- let approval;
1232
- let record;
1233
- try {
1234
- [approval, record] = await Promise.all([
1235
- readJson(join(runRoot, 'source/content-approval.json')),
1236
- readJson(join(runRoot, 'build-record.json')),
1237
- ]);
1238
- } catch (error) {
1239
- if (error?.code === 'ENOENT') return null;
1240
- throw error;
1241
- }
1242
- if (typeof record.runId !== 'string') {
1243
- throw codedError(
1244
- 'E_APPROVAL_RESUME',
1245
- 'The resumable run has no valid retained run identity.',
1246
- );
1247
- }
1248
- const approvalUnresolved = ['pending', 'rejected'].includes(approval.status);
1249
- const completedBeforeApproval = ['content', 'theme', 'render', 'qa'].every(
1250
- (id) =>
1251
- ['passed', 'warned', 'skipped'].includes(
1252
- record.stages?.find((stage) => stage.id === id)?.status,
1253
- ),
1254
- );
1255
- if (
1256
- !approvalUnresolved ||
1257
- !completedBeforeApproval ||
1258
- approval.runId !== record.runId
1259
- ) {
1260
- return null;
1261
- }
1262
- const resumableRun = {
1263
- runId: record.runId,
1264
- slug: normalized.slug,
1265
- outputRoot: canonicalOutputRoot,
1266
- runRoot: canonicalRunRoot,
1267
- requestPath: join(canonicalRunRoot, 'run-request.json'),
1268
- buildRecordPath: join(canonicalRunRoot, 'build-record.json'),
1269
- manifestPath: join(canonicalRunRoot, 'manifest.json'),
1270
- };
1271
- await verifySetPlanResumeToken(resumableRun, resumeToken);
1272
-
1273
- let persistedRequest;
1274
- try {
1275
- persistedRequest = await readJson(resumableRun.requestPath);
1276
- } catch (error) {
1277
- if (error?.code === 'ENOENT') return null;
1278
- throw error;
1279
- }
1280
- if (
1281
- typeof persistedRequest.outputRoot !== 'string' ||
1282
- (isAbsolute(persistedRequest.outputRoot) &&
1283
- persistedRequest.outputRoot !== canonicalOutputRoot)
1284
- ) {
1285
- throw codedError(
1286
- 'E_APPROVAL_RESUME',
1287
- 'The resumable run does not match the original canonical output root.',
1288
- );
1289
- }
1290
-
1291
- const currentPersistedRequest = canonicalPersistedRunRequest(normalized, {
1292
- outputRoot: canonicalOutputRoot,
1293
- });
1294
- if (
1295
- canonicalHash(persistedRequest) !== canonicalHash(currentPersistedRequest)
1296
- ) {
1297
- throw codedError(
1298
- 'E_APPROVAL_RESUME',
1299
- 'The resumable run does not match the complete canonical request.',
1300
- );
1301
- }
1302
-
1303
- const resumedRequest = structuredClone(currentPersistedRequest);
1304
- if (
1305
- normalized.theme?.artDirection !== undefined &&
1306
- resumedRequest.theme.artDirection === undefined
1307
- ) {
1308
- resumedRequest.theme.artDirection = normalized.theme.artDirection;
1309
- }
1310
- return {
1311
- ...resumableRun,
1312
- request: resumedRequest,
1313
- };
1314
- }
1315
-
1316
- function isStrictDescendant(root, candidate) {
1317
- const pathFromRoot = relative(root, candidate);
1318
- return (
1319
- pathFromRoot.length > 0 &&
1320
- pathFromRoot !== '..' &&
1321
- !pathFromRoot.startsWith(`..${sep}`) &&
1322
- !isAbsolute(pathFromRoot)
1323
- );
1324
- }
1325
-
1326
- async function hydrateResumableState(state) {
1327
- const [factBase, approval, theme, record] = await Promise.all([
1328
- readJson(join(state.run.runRoot, 'source/fact-base.json')),
1329
- readContentApproval(state.run).catch((error) => {
1330
- throw codedError(
1331
- 'E_APPROVAL_RESUME',
1332
- `Retained content approval is invalid: ${error.message}`,
1333
- );
1334
- }),
1335
- readJson(join(state.run.runRoot, 'theme.resolved.json')),
1336
- readJson(state.run.buildRecordPath),
1337
- ]);
1338
- for (const [kind, value] of [
1339
- ['fact-base', factBase],
1340
- ['theme', theme],
1341
- ['build-record', record],
1342
- ]) {
1343
- const validation = validateContract(kind, value);
1344
- if (!validation.valid) {
1345
- throw codedError(
1346
- 'E_APPROVAL_RESUME',
1347
- `Retained ${kind} record is invalid during approval resume.`,
1348
- );
1349
- }
1350
- }
1351
- const retainedPlan = await readSetPlanRecords(state.run, {
1352
- factBase,
1353
- recipe: state.recipe,
1354
- });
1355
- const portfolioValidation = validatePlannedPortfolio(
1356
- state.recipe,
1357
- retainedPlan.plan.portfolio,
1358
- );
1359
- if (!portfolioValidation.valid) {
1360
- throw codedError(
1361
- 'E_APPROVAL_RESUME',
1362
- `Retained set-plan portfolio is invalid: ${portfolioValidation.errors.join('; ')}`,
1363
- );
1364
- }
1365
- state.factBase = factBase;
1366
- state.setPlan = retainedPlan.plan;
1367
- state.setPlanPaths = retainedPlan.paths;
1368
- state.theme = theme;
1369
- state.themeWarnings = [];
1370
- state.resumedApprovalStatus = approval.status;
1371
- // Reopened stages rerun against the corrected content, so carrying their
1372
- // prior warnings forward would outlive the fix that resolved them.
1373
- const rerunning = approval.status === 'rejected' ? REOPENED_ON_REJECTION : [];
1374
- state.warnings.push(
1375
- ...record.stages
1376
- .filter(({ id }) => !rerunning.includes(id))
1377
- .flatMap(({ warnings = [] }) =>
1378
- warnings.filter((warning) => !warning.startsWith('stage-reopened:')),
1379
- ),
1380
- );
1381
- state.inputHashes = inputHashes(state.factBase);
1382
- state.factBaseHash = canonicalHash(state.factBase);
1383
- state.contentModels = [];
1384
- const persistedArtifacts = validateResumedArtifactBindings(state, approval);
1385
- for (const persisted of persistedArtifacts) {
1386
- const authorPath = await resolveRootConfinedPath(
1387
- state.run.runRoot,
1388
- persisted.authorResultPath,
1389
- );
1390
- if (!authorPath.valid) {
1391
- throw codedError(
1392
- 'E_APPROVAL_RESUME',
1393
- `Retained author path for ${persisted.artifactId} is not confined to the run.`,
1394
- );
1395
- }
1396
- const authorResult = await readJson(authorPath.absolutePath);
1397
- const validation = validateContract('author-result/v2', authorResult);
1398
- if (!validation.valid || authorResult.artifactId !== persisted.artifactId) {
1399
- throw codedError(
1400
- 'E_APPROVAL_RESUME',
1401
- `Retained author result identity for ${persisted.artifactId} is invalid.`,
1402
- );
1403
- }
1404
- }
1405
- const confinedContent = new Map();
1406
- for (const persisted of persistedArtifacts) {
1407
- const contentPath = await resolveRootConfinedPath(
1408
- state.run.runRoot,
1409
- persisted.contentPath,
1410
- );
1411
- if (!contentPath.valid) {
1412
- throw codedError(
1413
- 'E_APPROVAL_RESUME',
1414
- `Retained content path for ${persisted.artifactId} is not confined to the run.`,
1415
- );
1416
- }
1417
- confinedContent.set(persisted.artifactId, contentPath.absolutePath);
1418
- }
1419
- for (const persisted of persistedArtifacts) {
1420
- const artifact = resolvedArtifactFromApproval(state.recipe, persisted);
1421
- if (artifact.authoring === 'html') {
1422
- artifact.shellContent = await readSkillFile(
1423
- `templates/${artifact.shell}.html`,
1424
- );
1425
- }
1426
- const content = await readFile(
1427
- confinedContent.get(persisted.artifactId),
1428
- 'utf8',
1429
- );
1430
- state.resolvedArtifacts.push(artifact);
1431
- state.authoredContent.set(artifact.id, content);
1432
- state.contentPaths.set(artifact.id, persisted.contentPath);
1433
- state.authorResultPaths.push(persisted.authorResultPath);
1434
- }
1435
- const links = expansionLinks(state.resolvedArtifacts);
1436
- for (const artifact of state.resolvedArtifacts) {
1437
- if (artifact.authoring !== 'markdown') continue;
1438
- state.contentModels.push(
1439
- markdownContentModel(
1440
- artifact,
1441
- state.run.slug,
1442
- state.authoredContent.get(artifact.id),
1443
- artifact.origin === 'floor' ? links : [],
1444
- ),
1445
- );
1446
- }
1447
- state.expansion.accepted = state.resolvedArtifacts
1448
- .filter(({ origin }) => origin === 'expansion')
1449
- .map((artifact) => ({
1450
- id: artifact.id,
1451
- profileId: artifact.profileId,
1452
- rationale: 'Persisted approved expansion artifact.',
1453
- status: 'accepted',
1454
- profile: expansionProfile(state.recipe, artifact.profileId),
1455
- }));
1456
- await hydrateRenderedState(state);
1457
- }
1458
-
1459
- async function hydrateRenderedState(state) {
1460
- state.rendered = [];
1461
- state.artifacts = [];
1462
- for (const artifact of state.resolvedArtifacts) {
1463
- const descriptor =
1464
- artifact.authoring === 'markdown'
1465
- ? await renderArtifact({
1466
- recipeArtifact: renderDescriptor(artifact),
1467
- content: state.contentModels.find(
1468
- ({ artifactId }) => artifactId === artifact.id,
1469
- ),
1470
- factBase: state.factBase,
1471
- theme: state.theme,
1472
- renderStrategy: state.renderStrategy,
1473
- ...(state.run.request.publicBaseUrl && {
1474
- publicBaseUrl: state.run.request.publicBaseUrl,
1475
- }),
1476
- })
1477
- : artisticRender(state, artifact);
1478
- const html = await readFile(
1479
- join(state.run.runRoot, descriptor.renderedPath),
1480
- 'utf8',
1481
- );
1482
- const rendered = { ...descriptor, html };
1483
- state.rendered.push(rendered);
1484
- state.artifacts.push(artifactRecord(state, rendered));
1485
- }
1486
- }
1487
-
1488
- function artifactRecord(state, rendered) {
1489
- return {
1490
- id: rendered.artifactId,
1491
- type: rendered.type,
1492
- contentPath: state.contentPaths.get(rendered.artifactId),
1493
- renderedPath: rendered.renderedPath,
1494
- mediaType: rendered.mediaType,
1495
- status: 'built',
1496
- hash: hashBytes(rendered.html),
1497
- rebuildable: false,
1498
- };
1499
- }
1500
-
1501
- async function readJson(path) {
1502
- return JSON.parse(await readFile(path, 'utf8'));
1503
- }
1504
-
1505
- export async function runExplainerCli(
1506
- argv = process.argv.slice(2),
1507
- io = console,
1508
- run = runExplainer,
1509
- ) {
1510
- try {
1511
- const parsed = await parseCli(argv);
1512
- const request = JSON.parse(await readFile(parsed.requestPath, 'utf8'));
1513
- const result = await run(request, parsed.options);
1514
- io.log(JSON.stringify(projectCliRunResult(result), null, 2));
1515
- return result.outcome === 'failed' ? 1 : 0;
1516
- } catch (error) {
1517
- io.error(
1518
- JSON.stringify(
1519
- {
1520
- outcome: 'failed',
1521
- reasons: [
1522
- localEvidenceReason(error) ??
1523
- evidenceReason('planning', 'pipeline-failure'),
1524
- ],
1525
- },
1526
- null,
1527
- 2,
1528
- ),
1529
- );
1530
- return 1;
1531
- }
1532
- }
1533
-
1534
- function projectCliRunResult(result) {
1535
- if (!isObject(result)) {
1536
- return {
1537
- outcome: 'failed',
1538
- reasons: [evidenceReason('finalization', 'pipeline-failure')],
1539
- };
1540
- }
1541
- const projected = {};
1542
- for (const key of [
1543
- 'runId',
1544
- 'runRoot',
1545
- 'manifestPath',
1546
- 'buildRecordPath',
1547
- 'outcome',
1548
- 'marking',
1549
- ]) {
1550
- if (typeof result[key] === 'string') projected[key] = result[key];
1551
- }
1552
- if (Array.isArray(result.warnings)) {
1553
- projected.warnings = retainedWarnings(result.warnings);
1554
- }
1555
- if (isObject(result.discovery)) {
1556
- projected.discovery = {
1557
- ...(Number.isInteger(result.discovery.rounds) && {
1558
- rounds: result.discovery.rounds,
1559
- }),
1560
- ...(Number.isInteger(result.discovery.findingCount) && {
1561
- findingCount: result.discovery.findingCount,
1562
- }),
1563
- ...(['not-requested', 'two-empty-rounds', 'hard-maximum'].includes(
1564
- result.discovery.reason,
1565
- ) && { reason: result.discovery.reason }),
1566
- };
1567
- }
1568
- if (isObject(result.approval)) {
1569
- projected.approval = pickStringFields(result.approval, [
1570
- 'status',
1571
- 'path',
1572
- 'marking',
1573
- 'resumeToken',
1574
- ]);
1575
- }
1576
- const reasons = projectEvidenceReasons(result.reasons);
1577
- if (reasons.length > 0) projected.reasons = reasons;
1578
- const visualReview = projectVisualReviewEvidence(result.visualReview);
1579
- if (visualReview) projected.visualReview = visualReview;
1580
- const publication = projectPublicationSummaryForCli(result.publication);
1581
- if (publication) projected.publication = publication;
1582
- return projected;
1583
- }
1584
-
1585
- function projectVisualReviewEvidence(value) {
1586
- if (!isObject(value)) return null;
1587
- const reasons = projectEvidenceReasons(value.reasons);
1588
- if (
1589
- value.schemaVersion !== 'explainer-kit.visual-review-evidence/v1' ||
1590
- typeof value.requestHash !== 'string' ||
1591
- ![1, 2].includes(value.attempt) ||
1592
- !['pass', 'correct', 'failed'].includes(value.disposition) ||
1593
- !Array.isArray(value.reasons) ||
1594
- reasons.length !== value.reasons.length
1595
- ) {
1596
- return null;
1597
- }
1598
- return {
1599
- schemaVersion: value.schemaVersion,
1600
- requestHash: value.requestHash,
1601
- attempt: value.attempt,
1602
- disposition: value.disposition,
1603
- reasons,
1604
- };
1605
- }
1606
-
1607
- function projectPublicationSummaryForCli(value) {
1608
- if (!isObject(value)) return null;
1609
- if (
1610
- value.schemaVersion !== 'explainer-kit.publish-summary/v1' &&
1611
- value.schemaVersion !== 'explainer-kit.publish-summary/v2'
1612
- ) {
1613
- return null;
1614
- }
1615
- return {
1616
- schemaVersion: value.schemaVersion,
1617
- ...(typeof value.receiptSchemaVersion === 'string' && {
1618
- receiptSchemaVersion: value.receiptSchemaVersion,
1619
- }),
1620
- ...(typeof value.publicAccess === 'string' && {
1621
- publicAccess: value.publicAccess,
1622
- }),
1623
- ...(Array.isArray(value.artifacts) && {
1624
- artifacts: value.artifacts.map((artifact) =>
1625
- isObject(artifact)
1626
- ? {
1627
- ...pickStringFields(artifact, [
1628
- 'relativePath',
1629
- 'publicUrl',
1630
- 's3Uri',
1631
- 'hash',
1632
- 'contentType',
1633
- ]),
1634
- ...(isObject(artifact.source) && {
1635
- source: pickStringFields(artifact.source, [
1636
- 'kind',
1637
- 'artifactId',
1638
- 'name',
1639
- ]),
1640
- }),
1641
- ...(isObject(artifact.objectVerification) && {
1642
- objectVerification: pickClosedVerification(
1643
- artifact.objectVerification,
1644
- ),
1645
- }),
1646
- ...(isObject(artifact.publicVerification) && {
1647
- publicVerification: pickClosedVerification(
1648
- artifact.publicVerification,
1649
- ),
1650
- }),
1651
- }
1652
- : {},
1653
- ),
1654
- }),
1655
- };
1656
- }
1657
-
1658
- function pickClosedVerification(value) {
1659
- return {
1660
- ...pickStringFields(value, ['status', 'method', 'hash']),
1661
- ...(Number.isInteger(value.httpStatus) && {
1662
- httpStatus: value.httpStatus,
1663
- }),
1664
- };
1665
- }
1666
-
1667
- function projectEvidenceReasons(value) {
1668
- if (!Array.isArray(value)) return [];
1669
- return value.flatMap((reason) => {
1670
- if (
1671
- !isObject(reason) ||
1672
- ![
1673
- 'planning',
1674
- 'authoring',
1675
- 'rendering',
1676
- 'link-validation',
1677
- 'browser-review',
1678
- 'visual-review',
1679
- 'durability',
1680
- 'finalization',
1681
- ].includes(reason.stage) ||
1682
- ![
1683
- 'finding',
1684
- 'provider-failure',
1685
- 'pipeline-failure',
1686
- 'superseded',
1687
- ].includes(reason.kind) ||
1688
- !Number.isInteger(reason.count) ||
1689
- reason.count < 1 ||
1690
- reason.count > 50
1691
- ) {
1692
- return [];
1693
- }
1694
- return [
1695
- {
1696
- stage: reason.stage,
1697
- kind: reason.kind,
1698
- ...(typeof reason.artifactId === 'string' && {
1699
- artifactId: reason.artifactId,
1700
- }),
1701
- count: reason.count,
1702
- },
1703
- ];
1704
- });
1705
- }
1706
-
1707
- function pickStringFields(value, keys) {
1708
- return Object.fromEntries(
1709
- keys.flatMap((key) =>
1710
- typeof value[key] === 'string' ? [[key, value[key]]] : [],
1711
- ),
1712
- );
1713
- }
1714
-
1715
- function isObject(value) {
1716
- return value !== null && typeof value === 'object' && !Array.isArray(value);
1717
- }
1718
-
1719
- async function executeStage(run, id, options, operation) {
1720
- await updateBuildRecord(run, { id, status: 'running' });
1721
- try {
1722
- await options.hooks?.beforeStage?.(id, run);
1723
- const result = (await operation()) ?? {};
1724
- await options.hooks?.afterStage?.(id, run);
1725
- return updateBuildRecord(run, {
1726
- id,
1727
- status: result.status ?? 'passed',
1728
- outputPaths: result.outputPaths ?? [],
1729
- ...(result.warnings !== undefined && { warnings: result.warnings }),
1730
- });
1731
- } catch (error) {
1732
- const projected =
1733
- localEvidenceReason(error) ??
1734
- evidenceReason(evidenceStageForBuildStage(id), 'pipeline-failure');
1735
- await updateBuildRecord(run, {
1736
- id,
1737
- status: 'failed',
1738
- error: true,
1739
- });
1740
- throw withEvidenceReason(
1741
- codedError(stageErrorCode(id), `The ${id} stage failed.`),
1742
- projected.stage,
1743
- projected.kind,
1744
- );
1745
- }
1746
- }
1747
-
1748
- async function buildFactBase(binding, options, now) {
1749
- if (binding.mode === 'supplied') {
1750
- const factBase = JSON.parse(await readFile(binding.path, 'utf8'));
1751
- return processFactBase(
1752
- { mode: 'supplied', freshnessPolicy: binding.freshnessPolicy, factBase },
1753
- {
1754
- now: now(),
1755
- ...(options.maxFactAgeMs && { maxAgeMs: options.maxFactAgeMs }),
1756
- },
1757
- );
1758
- }
1759
-
1760
- const sourceDocuments = await Promise.all(
1761
- binding.sources.map(async (source) => {
1762
- if (
1763
- source.kind !== 'file' &&
1764
- typeof options.sourceLoader !== 'function'
1765
- ) {
1766
- throw codedError(
1767
- 'E_FACT_BASE',
1768
- `Core file loading cannot resolve source kind ${source.kind}; supply an explicit sourceLoader.`,
1769
- );
1770
- }
1771
- const raw = options.sourceLoader
1772
- ? await options.sourceLoader(source)
1773
- : JSON.parse(await readFile(source.locator, 'utf8'));
1774
- if (!Array.isArray(raw.claims)) {
1775
- throw codedError(
1776
- 'E_FACT_BASE',
1777
- `Federated source ${source.id} must provide a claims array.`,
1778
- );
1779
- }
1780
- const serialized = JSON.stringify(raw);
1781
- return {
1782
- source: {
1783
- ...source,
1784
- ...(options.sourceProvenance?.[source.id] ?? {}),
1785
- hash: raw.sourceHash ?? hashBytes(serialized),
1786
- observedAt: raw.observedAt ?? now(),
1787
- },
1788
- claims: raw.claims,
1789
- };
1790
- }),
1791
- );
1792
- return processFactBase(
1793
- {
1794
- mode: 'federated',
1795
- freshnessPolicy: binding.freshnessPolicy,
1796
- sourceDocuments,
1797
- overrides: options.overrides ?? [],
1798
- },
1799
- { critic: options.critic, now: now() },
1800
- );
1801
- }
1802
-
1803
- async function runDiscovery(recipe, factBase, options) {
1804
- if (typeof options.discover !== 'function') {
1805
- return { rounds: 0, findings: [], reason: 'not-requested' };
1806
- }
1807
- const findings = [];
1808
- const counts = [];
1809
- while (!shouldStopDiscovery(recipe, counts)) {
1810
- const round = counts.length + 1;
1811
- const roundFindings = await options.discover({
1812
- round,
1813
- recipe: structuredClone(recipe),
1814
- factBase: structuredClone(factBase),
1815
- });
1816
- if (!Array.isArray(roundFindings)) {
1817
- throw codedError('E_CONTENT', 'Discovery callback must return an array.');
1818
- }
1819
- counts.push(roundFindings.length);
1820
- findings.push(...roundFindings);
1821
- }
1822
- const emptyLimit = recipe.discoveryLimits.consecutiveNoNewFindingsRounds;
1823
- const stoppedEmpty =
1824
- counts.slice(-emptyLimit).length === emptyLimit &&
1825
- counts.slice(-emptyLimit).every((count) => count === 0);
1826
- return {
1827
- rounds: counts.length,
1828
- findings,
1829
- reason: stoppedEmpty ? 'two-empty-rounds' : 'hard-maximum',
1830
- };
1831
- }
1832
-
1833
- async function executeDurabilityAndPublish(state, options, now) {
1834
- const strategy = state.run.request.durability?.strategy ?? 'none';
1835
- if (strategy === 'none') {
1836
- await updateBuildRecord(state.run, { id: 'durability', status: 'skipped' });
1837
- await updateBuildRecord(state.run, { id: 'publish', status: 'skipped' });
1838
- return false;
1839
- }
1840
-
1841
- if (strategy === 'commit') {
1842
- await updateBuildRecord(state.run, { id: 'durability', status: 'running' });
1843
- await persistManifest(state, now());
1844
- if (typeof options.durability !== 'function') {
1845
- throw codedError(
1846
- 'E_DURABILITY',
1847
- 'Commit durability was requested without a durability callback.',
1848
- );
1849
- }
1850
- let providerFailed = false;
1851
- try {
1852
- await options.durability({
1853
- runRoot: state.run.runRoot,
1854
- manifestPath: state.run.manifestPath,
1855
- buildRecordPath: state.run.buildRecordPath,
1856
- });
1857
- } catch {
1858
- providerFailed = true;
1859
- }
1860
- let inventoryFailed = false;
1861
- try {
1862
- const manifest = JSON.parse(
1863
- await readFile(state.run.manifestPath, 'utf8'),
1864
- );
1865
- await enforceRetainedRunPackage(state, manifest);
1866
- } catch {
1867
- inventoryFailed = true;
1868
- }
1869
- if (providerFailed || inventoryFailed) {
1870
- await updateBuildRecord(state.run, {
1871
- id: 'durability',
1872
- status: 'failed',
1873
- error: true,
1874
- });
1875
- throw withEvidenceReason(
1876
- codedError('E_DURABILITY', 'Durability provider failed.'),
1877
- 'durability',
1878
- providerFailed ? 'provider-failure' : 'pipeline-failure',
1879
- );
1880
- }
1881
- await updateBuildRecord(state.run, {
1882
- id: 'durability',
1883
- status: 'warned',
1884
- warnings: ['durability-evidence-required'],
1885
- });
1886
- await updateBuildRecord(state.run, { id: 'publish', status: 'skipped' });
1887
- return false;
1888
- }
1889
-
1890
- await updateBuildRecord(state.run, { id: 'durability', status: 'skipped' });
1891
- await updateBuildRecord(state.run, { id: 'publish', status: 'running' });
1892
- if (typeof options.publish !== 'function') {
1893
- throw codedError(
1894
- 'E_PUBLISH',
1895
- 'Publish durability was requested without an explicit publisher callback.',
1896
- );
1897
- }
1898
- const finalizedAt = now();
1899
- const finalizedManifest = await persistManifest(state, finalizedAt);
1900
- let providerFailed = false;
1901
- let publishedReceipt;
1902
- try {
1903
- const buildRecord = JSON.parse(
1904
- await readFile(state.run.buildRecordPath, 'utf8'),
1905
- );
1906
- assertManifestPublishable(finalizedManifest, { buildRecord });
1907
- const receipt = await options.publish({
1908
- request: structuredClone(state.run.request.durability.publish),
1909
- runRoot: state.run.runRoot,
1910
- manifestPath: state.run.manifestPath,
1911
- });
1912
- const schemaValidation = validateContract('publish-receipt', receipt);
1913
- if (!schemaValidation.valid) {
1914
- throw codedError(
1915
- 'E_PUBLISH',
1916
- `Publisher returned an invalid receipt: ${schemaValidation.errors[0].message}`,
1917
- );
1918
- }
1919
- const crossRecordValidation = validateContract(
1920
- 'publish-receipt',
1921
- receipt,
1922
- publicationValidationContext(receipt, finalizedManifest),
1923
- );
1924
- if (!crossRecordValidation.valid) {
1925
- throw codedError(
1926
- 'E_PUBLISH',
1927
- `Publisher returned an invalid receipt: ${crossRecordValidation.errors[0].message}`,
1928
- );
1929
- }
1930
- publishedReceipt = receipt;
1931
- } catch {
1932
- providerFailed = true;
1933
- }
1934
- // Everything below the provider boundary is local work. Folding it into the
1935
- // try above classified a failed local write, build-record update or manifest
1936
- // rewrite as `provider-failure`, blaming the destination for a defect on this
1937
- // machine.
1938
- let pipelineFailed = false;
1939
- if (!providerFailed) {
1940
- try {
1941
- state.publishReceiptPath = 'publish-receipt.json';
1942
- await writeJsonAtomic(
1943
- state.run.runRoot,
1944
- state.publishReceiptPath,
1945
- publishedReceipt,
1946
- );
1947
- state.publication = publicationSummary(publishedReceipt);
1948
- await updateBuildRecord(state.run, {
1949
- id: 'publish',
1950
- status: 'warned',
1951
- warnings: ['publish-receipt-evidence-required'],
1952
- });
1953
- await persistManifest(state, finalizedAt);
1954
- } catch {
1955
- pipelineFailed = true;
1956
- }
1957
- }
1958
- let inventoryFailed = false;
1959
- try {
1960
- const manifest = JSON.parse(await readFile(state.run.manifestPath, 'utf8'));
1961
- await enforceRetainedRunPackage(state, manifest);
1962
- } catch {
1963
- inventoryFailed = true;
1964
- }
1965
- if (providerFailed || pipelineFailed || inventoryFailed) {
1966
- await updateBuildRecord(state.run, {
1967
- id: 'publish',
1968
- status: 'failed',
1969
- error: true,
1970
- });
1971
- throw withEvidenceReason(
1972
- codedError(
1973
- 'E_PUBLISH',
1974
- providerFailed
1975
- ? 'Publication provider failed.'
1976
- : 'Publication failed after upload while finalizing local records.',
1977
- ),
1978
- 'durability',
1979
- providerFailed ? 'provider-failure' : 'pipeline-failure',
1980
- );
1981
- }
1982
- return true;
1983
- }
1984
-
1985
- async function persistManifest(state, createdAt) {
1986
- const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
1987
- const immutableHashes = await immutableHashesFor(state);
1988
- let manifest = manifestFor(state, record, createdAt, immutableHashes);
1989
- const publicBaseUrl =
1990
- state.run.request.publicBaseUrl ??
1991
- state.run.request.durability?.publish?.publicBaseUrl;
1992
- if (publicBaseUrl) {
1993
- // Must match the catalog the connector builds and uploads byte for byte,
1994
- // so the verification policy is resolved from the same request fields.
1995
- const publicAccess =
1996
- state.run.request.publicAccess ??
1997
- state.run.request.durability?.publish?.publicAccess;
1998
- const catalogPath = initiativeCatalogPath(manifest.slug);
1999
- await writeJsonAtomic(
2000
- state.run.runRoot,
2001
- catalogPath,
2002
- catalogFromManifest(manifest, publicBaseUrl, { publicAccess }),
2003
- );
2004
- manifest = manifestFor(state, record, createdAt, {
2005
- ...immutableHashes,
2006
- [catalogPath]: hashBytes(
2007
- await readFile(join(state.run.runRoot, catalogPath)),
2008
- ),
2009
- });
2010
- }
2011
- await writeManifestAtomic(state.run, manifest);
2012
- return manifest;
2013
- }
2014
-
2015
- async function inventoryManifestFor(state, createdAt) {
2016
- const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
2017
- return manifestFor(state, record, createdAt, await immutableHashesFor(state));
2018
- }
2019
-
2020
- async function enforceRetainedRunPackage(
2021
- state,
2022
- manifest,
2023
- {
2024
- acceptCleanedUnexpected = false,
2025
- failureStage,
2026
- includeManifest = true,
2027
- includeTerminalEvidence = false,
2028
- } = {},
2029
- ) {
2030
- try {
2031
- return await enforceRunPackageInventory(state.run.runRoot, manifest, {
2032
- includeManifest,
2033
- includeTerminalEvidence,
2034
- removeUnexpected: true,
2035
- });
2036
- } catch (error) {
2037
- if (acceptCleanedUnexpected) {
2038
- await enforceRunPackageInventory(state.run.runRoot, manifest, {
2039
- includeManifest,
2040
- includeTerminalEvidence,
2041
- });
2042
- return;
2043
- }
2044
- if (failureStage) {
2045
- await updateBuildRecord(state.run, {
2046
- id: failureStage,
2047
- status: 'failed',
2048
- error: true,
2049
- });
2050
- }
2051
- throw error;
2052
- }
2053
- }
2054
-
2055
- async function persistFailureManifest(state, _error, createdAt) {
2056
- const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
2057
- const recordedIds = new Set(state.artifacts.map(({ id }) => id));
2058
- state.artifacts.push(
2059
- ...recipeFloor(state.recipe)
2060
- .filter(({ id }) => !recordedIds.has(id))
2061
- .map((artifact) => ({
2062
- id: artifact.id,
2063
- type: artifact.type,
2064
- contentPath: state.contentPaths.get(artifact.id),
2065
- status: 'failed',
2066
- rebuildable: false,
2067
- failure: {
2068
- code: 'E_ARTIFACT_BUILD',
2069
- message: 'Artifact construction failed.',
2070
- recovery: ['Correct the failed stage and start a new run.'],
2071
- },
2072
- })),
2073
- );
2074
- return writeManifestAtomic(
2075
- state.run,
2076
- manifestFor(state, record, createdAt, await immutableHashesFor(state)),
2077
- );
2078
- }
2079
-
2080
- function manifestFor(state, buildRecord, createdAt, immutableHashes) {
2081
- return {
2082
- schemaVersion: 'explainer-kit.manifest/v1',
2083
- runId: state.run.runId,
2084
- slug: state.run.slug,
2085
- recipe: {
2086
- id: state.recipe.id,
2087
- version: state.recipe.version,
2088
- },
2089
- createdAt,
2090
- source: {
2091
- factBasePath: 'source/fact-base.json',
2092
- factBaseHash: state.factBaseHash,
2093
- inputHashes: state.inputHashes,
2094
- backlinks: manifestSourceBacklinks(state.factBase),
2095
- ...(state.authorResultPaths.length > 0 && {
2096
- authorResultPaths: state.authorResultPaths,
2097
- }),
2098
- },
2099
- theme: {
2100
- path: 'theme.resolved.json',
2101
- hash: canonicalHash(state.theme),
2102
- derived: state.theme.provenance.derived,
2103
- },
2104
- artifacts: state.artifacts,
2105
- immutableHashes,
2106
- outcome: buildRecord.outcome,
2107
- buildRecord: {
2108
- path: 'build-record.json',
2109
- hash: canonicalHash(buildRecord),
2110
- },
2111
- warnings: retainedWarnings(state.warnings),
2112
- };
2113
- }
2114
-
2115
- function manifestSourceBacklinks(factBase) {
2116
- const backlinks = [
2117
- ...(factBase?.sources ?? [])
2118
- .filter(({ url }) => typeof url === 'string')
2119
- .map(({ id, url }) => ({ sourceId: id, url })),
2120
- ...(factBase?.claims ?? [])
2121
- .flatMap(({ citations }) => citations ?? [])
2122
- .filter(({ url }) => typeof url === 'string')
2123
- .map(({ sourceId, url }) => ({ sourceId, url })),
2124
- ...(factBase?.unresolvedClaims ?? [])
2125
- .flatMap(({ citations }) => citations ?? [])
2126
- .filter(({ url }) => typeof url === 'string')
2127
- .map(({ sourceId, url }) => ({ sourceId, url })),
2128
- ];
2129
- return [
2130
- ...new Map(
2131
- backlinks
2132
- .sort(
2133
- (left, right) =>
2134
- left.sourceId.localeCompare(right.sourceId) ||
2135
- left.url.localeCompare(right.url),
2136
- )
2137
- .map((backlink) => [`${backlink.sourceId}\0${backlink.url}`, backlink]),
2138
- ).values(),
2139
- ];
2140
- }
2141
-
2142
- async function createAuthoredContent(state, options, now) {
2143
- const author = options.author;
2144
- if (typeof author !== 'function') {
2145
- throw codedError(
2146
- 'E_AUTHOR_REQUIRED',
2147
- 'Explainer runs require an explicit author callback in both modes.',
2148
- );
2149
- }
2150
- const trust = authorTrustContext(options, now);
2151
- const artifacts = plannedArtifacts(state.recipe, state.setPlan);
2152
- state.expansion = {
2153
- valid: true,
2154
- accepted: artifacts
2155
- .filter(({ origin }) => origin === 'expansion')
2156
- .map((artifact) => ({
2157
- id: artifact.id,
2158
- profileId: artifact.profileId,
2159
- rationale: artifact.plannedArtifact.justification.rationale,
2160
- status: 'accepted',
2161
- profile: expansionProfile(state.recipe, artifact.profileId),
2162
- })),
2163
- rejected: [],
2164
- warnings: [],
2165
- errors: [],
2166
- };
2167
- const authored = [];
2168
- const artifactLinkTables = new Map(
2169
- artifacts.map((artifact) => [
2170
- artifact.id,
2171
- canonicalArtifactLinks(artifacts, artifact.id, state.run.slug),
2172
- ]),
2173
- );
2174
- for (const artifact of artifacts) {
2175
- const item = await authorArtifact(
2176
- state,
2177
- artifact,
2178
- author,
2179
- trust,
2180
- artifactLinkTables.get(artifact.id),
2181
- );
2182
- if ((item.result.proposedArtifacts ?? []).length > 0) {
2183
- throw codedError(
2184
- 'E_AUTHOR_RESULT',
2185
- `Artifact ${artifact.id} cannot change the validated set plan.`,
2186
- );
2187
- }
2188
- authored.push(item);
2189
- }
2190
- state.resolvedArtifacts = authored.map(({ artifact }) => artifact);
2191
- const links = expansionLinks(state.resolvedArtifacts);
2192
- for (const item of authored) {
2193
- await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
2194
- await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
2195
- state.authorResultPaths.push(item.resultPath);
2196
- state.contentPaths.set(item.artifact.id, item.contentPath);
2197
- state.authoredContent.set(item.artifact.id, item.content);
2198
- if (item.artifact.authoring === 'markdown') {
2199
- state.contentModels.push(
2200
- assertValidContentModel(
2201
- state.recipe,
2202
- markdownContentModel(
2203
- item.artifact,
2204
- state.run.slug,
2205
- item.content,
2206
- item.artifact.origin === 'floor' ? links : [],
2207
- ),
2208
- item.artifact,
2209
- ),
2210
- );
2211
- }
2212
- }
2213
- }
2214
-
2215
- // Expansion artifacts are not recipe-floor entries, so only floor content models
2216
- // can be checked against the floor-scoped contract.
2217
- function assertValidContentModel(recipe, model, artifact) {
2218
- if (artifact.origin !== 'floor') return model;
2219
- const result = validateContentModel(recipe, model);
2220
- if (!result.valid) {
2221
- throw codedError(
2222
- 'E_CONTENT_MODEL',
2223
- `Authored content for ${artifact.id} violates the narrative contract: ${result.errors.join('; ')}`,
2224
- );
2225
- }
2226
- return model;
2227
- }
2228
-
2229
- // Provenance authenticity cannot come from the party being identified, so
2230
- // identity and method are bound to trusted caller configuration and the
2231
- // generation time is stamped from the run's injected clock.
2232
- function authorTrustContext(options, now) {
2233
- const generatedAt = now();
2234
- const declared = options.authorProvenance;
2235
- if (declared === undefined) return { generatedAt, bound: null };
2236
- const valid =
2237
- typeof declared === 'object' &&
2238
- declared !== null &&
2239
- !Array.isArray(declared) &&
2240
- typeof declared.authorId === 'string' &&
2241
- declared.authorId.length > 0 &&
2242
- (declared.method === undefined ||
2243
- (typeof declared.method === 'string' && declared.method.length > 0));
2244
- if (!valid) {
2245
- throw codedError(
2246
- 'E_AUTHOR_PROVENANCE',
2247
- 'Trusted author provenance requires a non-empty authorId and, when present, a non-empty method.',
2248
- );
2249
- }
2250
- return {
2251
- generatedAt,
2252
- bound: {
2253
- authorId: declared.authorId,
2254
- ...(declared.method !== undefined && { method: declared.method }),
2255
- },
2256
- };
2257
- }
2258
-
2259
- function resolveAuthorProvenance(claimed, trust, artifactId) {
2260
- if ('trust' in claimed) {
2261
- throw codedError(
2262
- 'E_AUTHOR_PROVENANCE',
2263
- `Author result for ${artifactId} must not assert a provenance trust level; the core stamps it.`,
2264
- );
2265
- }
2266
- if (trust.bound === null) {
2267
- return {
2268
- ...claimed,
2269
- generatedAt: trust.generatedAt,
2270
- trust: 'self-asserted',
2271
- };
2272
- }
2273
- if (
2274
- claimed.authorId !== trust.bound.authorId ||
2275
- (trust.bound.method !== undefined && claimed.method !== trust.bound.method)
2276
- ) {
2277
- throw codedError(
2278
- 'E_AUTHOR_PROVENANCE',
2279
- `Author result for ${artifactId} claims provenance that does not match the trusted caller context.`,
2280
- );
2281
- }
2282
- return {
2283
- ...trust.bound,
2284
- generatedAt: trust.generatedAt,
2285
- trust: 'caller-bound',
2286
- };
2287
- }
2288
-
2289
- async function authorArtifact(
2290
- state,
2291
- artifact,
2292
- author,
2293
- trust,
2294
- artifactLinks,
2295
- correctionContext,
2296
- ) {
2297
- const [brief, visualAuthoringGuidance, shellContent] = await Promise.all([
2298
- readSkillFile(artifact.briefRef),
2299
- readSkillFile('references/visual-authoring.md'),
2300
- artifact.authoring === 'html'
2301
- ? readSkillFile(`templates/${artifact.shell}.html`)
2302
- : undefined,
2303
- ]);
2304
- const resolvedArtifact = {
2305
- ...artifact,
2306
- ...(shellContent && { shellContent }),
2307
- };
2308
- const requiredNarrative =
2309
- artifact.origin === 'floor'
2310
- ? recipeRequiredNarrative(state.recipe, artifact.id)
2311
- : [];
2312
- const plannedDiagrams = diagramAnalyses(artifact.plannedArtifact.draft);
2313
- const graphSemantics = graphSemanticsForArtisticAuthor(plannedDiagrams);
2314
- if (
2315
- graphSemantics.length > 0 &&
2316
- resolveDiagramRenderingRoute(state.recipe, artifact, plannedDiagrams) !==
2317
- 'artistic'
2318
- ) {
2319
- throw codedError(
2320
- 'E_DIAGRAM_TOPOLOGY',
2321
- `Artifact ${artifact.id} cannot preserve its planner-owned non-linear graph through inline rendering.`,
2322
- );
2323
- }
2324
- const authorRequest = {
2325
- schemaVersion: 'explainer-kit.author-request/v3',
2326
- artifactId: artifact.id,
2327
- artifactType: artifact.type,
2328
- authoring: artifact.authoring,
2329
- brief,
2330
- visualAuthoringGuidance,
2331
- factBase: structuredClone(state.factBase),
2332
- ...(shellContent && { shell: shellContent }),
2333
- theme: structuredClone(state.theme),
2334
- setContext: structuredClone(state.setPlan),
2335
- plannedArtifact: structuredClone(artifact.plannedArtifact),
2336
- artifactLinks: structuredClone(artifactLinks),
2337
- ...(graphSemantics.length > 0 && {
2338
- graphSemantics: structuredClone(graphSemantics),
2339
- }),
2340
- ...(artifact.origin === 'floor' &&
2341
- requiredNarrative.length > 0 && {
2342
- floor: { requiredNarrative },
2343
- }),
2344
- };
2345
- const requestValidation = validateContract(
2346
- 'author-request/v3',
2347
- authorRequest,
2348
- );
2349
- if (!requestValidation.valid) {
2350
- throw codedError(
2351
- 'E_AUTHOR_REQUEST',
2352
- contractErrorMessage('author request', requestValidation.errors),
2353
- );
2354
- }
2355
-
2356
- let result;
2357
- try {
2358
- result =
2359
- correctionContext === undefined
2360
- ? await author(structuredClone(authorRequest))
2361
- : await author(
2362
- structuredClone(authorRequest),
2363
- structuredClone(correctionContext),
2364
- );
2365
- } catch {
2366
- throw withEvidenceReason(
2367
- codedError('E_AUTHOR', 'Artifact author provider failed.'),
2368
- 'authoring',
2369
- 'provider-failure',
2370
- );
2371
- }
2372
- const resultValidation = validateContract('author-result/v2', result);
2373
- if (!resultValidation.valid) {
2374
- throw codedError(
2375
- 'E_AUTHOR_RESULT',
2376
- contractErrorMessage('author result', resultValidation.errors),
2377
- );
2378
- }
2379
- const content = result.content?.[artifact.authoring];
2380
- if (result.artifactId !== artifact.id || typeof content !== 'string') {
2381
- throw codedError(
2382
- 'E_AUTHOR_RESULT',
2383
- `Author result for ${artifact.id} must match its identity and ${artifact.authoring} path.`,
2384
- );
2385
- }
2386
- if (artifact.authoring === 'markdown') {
2387
- const diagrams = diagramAnalyses(content);
2388
- if (
2389
- diagrams.length > 0 &&
2390
- resolveDiagramRenderingRoute(state.recipe, artifact, diagrams) ===
2391
- 'reject'
2392
- ) {
2393
- const features = [
2394
- ...new Set(
2395
- diagrams.flatMap(({ topology }) => topology?.features ?? []),
2396
- ),
2397
- ].join(', ');
2398
- throw codedError(
2399
- 'E_DIAGRAM_TOPOLOGY',
2400
- `Artifact ${artifact.id} contains ${features || 'non-linear'} diagram topology that requires artistic composition; inline rendering is rejected.`,
2401
- );
2402
- }
2403
- } else if (graphSemantics.length > 0) {
2404
- assertAuthoredGraphSemantics(content, graphSemantics);
2405
- }
2406
- const retained = {
2407
- ...structuredClone(result),
2408
- provenance: resolveAuthorProvenance(result.provenance, trust, artifact.id),
2409
- };
2410
- const retainedValidation = validateContract('author-result/v2', retained);
2411
- if (!retainedValidation.valid) {
2412
- throw codedError(
2413
- 'E_AUTHOR_PROVENANCE',
2414
- contractErrorMessage('retained author result', retainedValidation.errors),
2415
- );
2416
- }
2417
- const dumpCheck = checkSourceDumping({
2418
- authoredText: content,
2419
- sourceTexts: [
2420
- ...state.factBase.claims,
2421
- ...state.factBase.unresolvedClaims,
2422
- ].map(({ text }) => text),
2423
- });
2424
- state.qaErrors.push(
2425
- ...dumpCheck.issues.map((issue) => ({
2426
- code: issue.code,
2427
- message: issue.message,
2428
- })),
2429
- );
2430
-
2431
- return {
2432
- artifact: resolvedArtifact,
2433
- result: retained,
2434
- resultPath: `source/author/${artifact.id}.json`,
2435
- content,
2436
- contentPath: `source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
2437
- };
2438
- }
2439
-
2440
- function canonicalArtifactLinks(artifacts, currentArtifactId, slug) {
2441
- const paths = new Map(
2442
- artifacts.map((artifact) => [
2443
- artifact.id,
2444
- artifactPath(renderDescriptor(artifact), slug),
2445
- ]),
2446
- );
2447
- const currentPath = paths.get(currentArtifactId);
2448
- if (!currentPath) {
2449
- throw codedError(
2450
- 'E_AUTHOR_REQUEST',
2451
- `Cannot construct canonical links for unknown artifact ${currentArtifactId}.`,
2452
- );
2453
- }
2454
- return artifacts.map((artifact) => {
2455
- const sitePath = paths.get(artifact.id);
2456
- const href =
2457
- posix.relative(posix.dirname(currentPath), sitePath) ||
2458
- posix.basename(sitePath);
2459
- return {
2460
- artifactId: artifact.id,
2461
- artifactType: artifact.type,
2462
- sitePath,
2463
- href,
2464
- };
2465
- });
2466
- }
2467
-
2468
- function diagramAnalyses(markdown) {
2469
- const ast = parseMarkdown(markdown);
2470
- const diagrams = [];
2471
- const visit = (nodes) => {
2472
- for (const node of nodes) {
2473
- if (node.type === 'diagram') {
2474
- diagrams.push(parseDiagram(node.source));
2475
- }
2476
- if (Array.isArray(node.children)) {
2477
- visit(node.children);
2478
- }
2479
- }
2480
- };
2481
- visit(ast.children);
2482
- return diagrams;
2483
- }
2484
-
2485
- async function readSkillFile(relativePath) {
2486
- return readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8');
2487
- }
2488
-
2489
- function contractErrorMessage(label, errors) {
2490
- return `Invalid ${label}: ${errors
2491
- .map(({ path, message }) => `${path}: ${message}`)
2492
- .join('; ')}`;
2493
- }
2494
-
2495
- // renderArtifact validates an exact key set, so normalized v2 floor entries
2496
- // must be narrowed before they reach it.
2497
- function renderDescriptor(artifact) {
2498
- return {
2499
- id: artifact.id,
2500
- type: artifact.type,
2501
- template: artifact.template,
2502
- required: artifact.required,
2503
- origin: artifact.origin,
2504
- };
2505
- }
2506
-
2507
- const LEAD_SECTION_IDS = ['overview', 'introduction', 'lead'];
2508
-
2509
- function markdownContentModel(artifact, slug, markdown, artifactLinks) {
2510
- const titleMatch = markdown.match(/^# (.+)$/m);
2511
- const title = titleMatch?.[1]?.trim() ?? humanize(artifact.id);
2512
- const headings = [...markdown.matchAll(/^## (.+)$/gm)];
2513
- if (headings.length === 0) {
2514
- return contentModel({
2515
- artifact,
2516
- slug,
2517
- title,
2518
- artifactLinks,
2519
- sections: [{ id: 'overview', title: 'Overview', content: markdown }],
2520
- });
2521
- }
2522
-
2523
- // Prose between the document title and the first `##` is authored content,
2524
- // so it is carried as a leading section rather than silently dropped.
2525
- const bodyStart = titleMatch ? titleMatch.index + titleMatch[0].length : 0;
2526
- const lead = markdown.slice(bodyStart, headings[0].index).trim();
2527
- const authoredIds = new Set(
2528
- headings.map((heading) => slugify(heading[1].trim())),
2529
- );
2530
- const sections = headings.map((heading, index) => ({
2531
- id: slugify(heading[1].trim()),
2532
- title: heading[1].trim(),
2533
- content: markdown
2534
- .slice(
2535
- heading.index + heading[0].length,
2536
- headings[index + 1]?.index ?? markdown.length,
2537
- )
2538
- .trim(),
2539
- }));
2540
- if (lead.length > 0) {
2541
- const leadId =
2542
- LEAD_SECTION_IDS.find((candidate) => !authoredIds.has(candidate)) ??
2543
- 'lead';
2544
- sections.unshift({
2545
- id: leadId,
2546
- title: humanize(leadId),
2547
- content: lead,
2548
- });
2549
- }
2550
-
2551
- return contentModel({
2552
- artifact,
2553
- slug,
2554
- title,
2555
- artifactLinks,
2556
- sections: disambiguateSectionIds(sections),
2557
- });
2558
- }
2559
-
2560
- // A repeated heading is legitimate authoring, but duplicate anchors break
2561
- // navigation, so later collisions get a deterministic suffix.
2562
- function disambiguateSectionIds(sections) {
2563
- const used = new Map();
2564
- return sections.map((section) => {
2565
- const seen = used.get(section.id) ?? 0;
2566
- used.set(section.id, seen + 1);
2567
- if (seen === 0) return section;
2568
- let candidate = `${section.id}-${seen + 1}`;
2569
- let offset = seen + 1;
2570
- while (used.has(candidate)) {
2571
- offset += 1;
2572
- candidate = `${section.id}-${offset}`;
2573
- }
2574
- used.set(candidate, 1);
2575
- return { ...section, id: candidate };
2576
- });
2577
- }
2578
-
2579
- function contentModel({ artifact, slug, title, artifactLinks, sections }) {
2580
- return {
2581
- artifactId: artifact.id,
2582
- slug,
2583
- title,
2584
- description: `Authored ${humanize(artifact.id).toLowerCase()}.`,
2585
- eyebrow: 'Explainer Kit',
2586
- footer: 'Authored from the retained reconciled fact base.',
2587
- sections,
2588
- artifactLinks,
2589
- };
2590
- }
2591
-
2592
- function approvalArtifacts(state) {
2593
- return state.resolvedArtifacts.map((artifact) => ({
2594
- artifactId: artifact.id,
2595
- origin: artifact.origin,
2596
- ...(artifact.profileId && { profileId: artifact.profileId }),
2597
- authoring: artifact.authoring,
2598
- contentPath:
2599
- state.contentPaths.get(artifact.id) ??
2600
- `source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
2601
- authorResultPath:
2602
- state.authorResultPaths.find((path) =>
2603
- path.endsWith(`/${artifact.id}.json`),
2604
- ) ?? `source/author/${artifact.id}.json`,
2605
- }));
2606
- }
2607
-
2608
- function validateResumedArtifactBindings(state, approval) {
2609
- const expected = plannedArtifacts(state.recipe, state.setPlan).map(
2610
- (artifact) => ({
2611
- artifactId: artifact.id,
2612
- origin: artifact.origin,
2613
- ...(artifact.profileId && { profileId: artifact.profileId }),
2614
- authoring: artifact.authoring,
2615
- contentPath: `source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
2616
- authorResultPath: `source/author/${artifact.id}.json`,
2617
- }),
2618
- );
2619
- if (
2620
- !Array.isArray(approval.artifacts) ||
2621
- !Array.isArray(approval.authorResultPaths)
2622
- ) {
2623
- throw codedError(
2624
- 'E_APPROVAL_RESUME',
2625
- 'Approval resume requires retained artifact and author-result bindings.',
2626
- );
2627
- }
2628
- const persistedById = new Map(
2629
- approval.artifacts.map((artifact) => [artifact.artifactId, artifact]),
2630
- );
2631
- if (
2632
- persistedById.size !== expected.length ||
2633
- canonicalHash(approval.authorResultPaths) !==
2634
- canonicalHash(expected.map(({ authorResultPath }) => authorResultPath))
2635
- ) {
2636
- throw codedError(
2637
- 'E_APPROVAL_RESUME',
2638
- 'Approval artifact set does not match the retained set-plan portfolio.',
2639
- );
2640
- }
2641
- for (const artifact of expected) {
2642
- const persisted = persistedById.get(artifact.artifactId);
2643
- if (
2644
- persisted === undefined ||
2645
- canonicalHash(persisted) !== canonicalHash(artifact)
2646
- ) {
2647
- throw codedError(
2648
- 'E_APPROVAL_RESUME',
2649
- `Approval artifact binding for ${artifact.artifactId} has drifted from the retained set plan.`,
2650
- );
2651
- }
2652
- }
2653
- return expected;
2654
- }
2655
-
2656
- function expansionLinks(artifacts) {
2657
- return artifacts
2658
- .filter(({ origin }) => origin === 'expansion')
2659
- .map((artifact) => ({
2660
- id: artifact.id,
2661
- type: artifact.type,
2662
- label: humanize(artifact.id),
2663
- origin: 'expansion',
2664
- }));
2665
- }
2666
-
2667
- function expansionProfile(recipe, profileId) {
2668
- return recipeExpansion(recipe).profiles.find(
2669
- (profile) => profile.profileId === profileId,
2670
- );
2671
- }
2672
-
2673
- function resolvedArtifactFromApproval(recipe, persisted) {
2674
- if (persisted.origin === 'floor') {
2675
- const artifact = recipeFloor(recipe).find(
2676
- ({ id }) => id === persisted.artifactId,
2677
- );
2678
- if (!artifact) {
2679
- throw codedError(
2680
- 'E_APPROVAL_RESUME',
2681
- `Approval references unknown floor artifact ${persisted.artifactId}.`,
2682
- );
2683
- }
2684
- return {
2685
- ...artifact,
2686
- origin: 'floor',
2687
- shell: artifact.authoring === 'html' ? artifact.template : undefined,
2688
- };
2689
- }
2690
- const profile = expansionProfile(recipe, persisted.profileId);
2691
- if (!profile) {
2692
- throw codedError(
2693
- 'E_APPROVAL_RESUME',
2694
- `Approval references unknown expansion profile ${persisted.profileId}.`,
2695
- );
2696
- }
2697
- return {
2698
- id: persisted.artifactId,
2699
- type: profile.type,
2700
- authoring: profile.authoring,
2701
- briefRef: profile.briefRef,
2702
- shell: profile.shell,
2703
- template:
2704
- profile.authoring === 'markdown'
2705
- ? templateForType(profile.type)
2706
- : profile.shell,
2707
- required: false,
2708
- origin: 'expansion',
2709
- profileId: profile.profileId,
2710
- };
2711
- }
2712
-
2713
- function templateForType(type) {
2714
- return (
2715
- {
2716
- hub: 'house-style',
2717
- diagram: 'diagram-shell',
2718
- explainer: 'engineer-tour',
2719
- deck: 'deck-shell',
2720
- }[type] ?? 'house-style'
2721
- );
2722
- }
2723
-
2724
- function validateRecipeSources(recipe, binding) {
2725
- const primaryRole = recipe.sourceRoles[0]?.role;
2726
- const bindings =
2727
- binding.mode === 'supplied'
2728
- ? [{ role: primaryRole, kind: 'file', sourceSetId: 'supplied-fact-base' }]
2729
- : binding.sources.map((source) => ({
2730
- role: source.role,
2731
- kind: source.kind,
2732
- sourceSetId: source.sourceSetId,
2733
- }));
2734
- const result = validateSourceBindings(recipe, bindings);
2735
- if (!result.valid) {
2736
- throw codedError('E_FACT_BASE', result.errors.join('; '));
2737
- }
2738
- }
2739
-
2740
- async function immutableHashesFor(state) {
2741
- const paths = [
2742
- 'run-request.json',
2743
- 'source/fact-base.json',
2744
- 'source/fact-base.md',
2745
- 'source/content-approval.json',
2746
- ...state.setPlanPaths,
2747
- ...state.authorResultPaths,
2748
- ...state.contentPaths.values(),
2749
- ...state.browserEvidence.flatMap(({ screenshotPath, metricsPath }) => [
2750
- screenshotPath,
2751
- metricsPath,
2752
- ]),
2753
- ...state.visualReviewPaths,
2754
- ...(state.publishReceiptPath ? [state.publishReceiptPath] : []),
2755
- ...(state.theme ? ['theme.resolved.json'] : []),
2756
- ...state.artifacts
2757
- .filter(
2758
- ({ status, renderedPath }) =>
2759
- status === 'built' && typeof renderedPath === 'string',
2760
- )
2761
- .map(({ renderedPath }) => renderedPath),
2762
- ];
2763
- return Object.fromEntries(
2764
- await Promise.all(
2765
- [...new Set(paths)].map(async (path) => [
2766
- path,
2767
- hashBytes(await readFile(join(state.run.runRoot, path))),
2768
- ]),
2769
- ),
2770
- );
2771
- }
2772
-
2773
- function inputHashes(factBase) {
2774
- return Object.fromEntries(
2775
- factBase.sources.map(({ id, hash }) => [`inputs/${safeId(id)}`, hash]),
2776
- );
2777
- }
2778
-
2779
- function factBaseMarkdown(factBase) {
2780
- const confirmed = factBase.claims
2781
- .map(({ id, text }) => `- **${id}:** ${text}`)
2782
- .join('\n');
2783
- const unresolved = factBase.unresolvedClaims
2784
- .map(({ id, text, reason }) => `- **${id} (${reason}):** ${text}`)
2785
- .join('\n');
2786
- return `# Fact base\n\n## Confirmed claims\n\n${confirmed || '- None.'}\n\n## Unresolved claims\n\n${unresolved || '- None.'}\n`;
2787
- }
2788
-
2789
- function assertValidRequest(request) {
2790
- const result = validateContract('run-request', request);
2791
- if (!result.valid) {
2792
- throw codedError(
2793
- 'E_INPUT_SCHEMA',
2794
- result.errors
2795
- .map(({ path, code, message }) => `${path} [${code}]: ${message}`)
2796
- .join('; '),
2797
- );
2798
- }
2799
- }
2800
-
2801
- function terminalReasonsForReview(state) {
2802
- const visualReasons = state.visualReview?.evidence?.reasons ?? [];
2803
- if (visualReasons.length > 0) return structuredClone(visualReasons);
2804
- return [
2805
- state.reviewGateReason ??
2806
- evidenceReason('visual-review', 'pipeline-failure'),
2807
- ];
2808
- }
2809
-
2810
- function resultFor(state, failure = { failed: false }) {
2811
- return {
2812
- runId: state.run.runId,
2813
- runRoot: state.run.runRoot,
2814
- manifestPath: state.run.manifestPath,
2815
- buildRecordPath: state.run.buildRecordPath,
2816
- outcome: failure.failed
2817
- ? 'failed'
2818
- : state.approval?.canResume === false
2819
- ? 'incomplete'
2820
- : state.reviewGateBlocked
2821
- ? 'built-needs-review'
2822
- : 'built-not-durable',
2823
- ...(state.approval?.record?.marking && {
2824
- marking: state.approval.record.marking,
2825
- }),
2826
- warnings: retainedWarnings(state.warnings),
2827
- discovery: {
2828
- rounds: state.discovery.rounds,
2829
- findingCount: state.discovery.findings.length,
2830
- reason: state.discovery.reason,
2831
- },
2832
- ...(state.publication && { publication: state.publication }),
2833
- ...(state.approval && {
2834
- approval: {
2835
- status: state.approval.status,
2836
- path: state.approval.path,
2837
- ...(state.approval.record.marking && {
2838
- marking: state.approval.record.marking,
2839
- }),
2840
- ...(state.resumeToken && { resumeToken: state.resumeToken }),
2841
- },
2842
- }),
2843
- ...(state.visualReview && {
2844
- visualReview: structuredClone(state.visualReview.evidence),
2845
- }),
2846
- ...(failure.failed && {
2847
- reasons: structuredClone(failure.reasons),
2848
- }),
2849
- };
2850
- }
2851
-
2852
- function publicationSummary(receipt) {
2853
- if (receipt.schemaVersion === 'explainer-kit.publish-receipt/v2') {
2854
- return {
2855
- schemaVersion: 'explainer-kit.publish-summary/v2',
2856
- receiptSchemaVersion: receipt.schemaVersion,
2857
- publicAccess: receipt.publicAccess,
2858
- artifacts: receipt.artifacts.map((artifact) => structuredClone(artifact)),
2859
- };
2860
- }
2861
- return {
2862
- schemaVersion: 'explainer-kit.publish-summary/v1',
2863
- receiptSchemaVersion: receipt.schemaVersion,
2864
- publicAccess: 'public',
2865
- artifacts: receipt.artifacts.map(({ relativePath, publicUrl }) => ({
2866
- relativePath,
2867
- publicUrl,
2868
- })),
2869
- };
2870
- }
2871
-
2872
- function publicationValidationContext(receipt, manifest) {
2873
- if (receipt.schemaVersion !== 'explainer-kit.publish-receipt/v2') {
2874
- return { manifest };
2875
- }
2876
- let catalog;
2877
- try {
2878
- // `publicAccess` is a receipt/v2 field; a v1 receipt returns above, and the
2879
- // connector resolves v1 publish requests to 'public' for the same reason.
2880
- catalog = catalogFromManifest(manifest, receipt.roots.publicBaseUrl, {
2881
- publicAccess: receipt.publicAccess,
2882
- });
2883
- } catch (error) {
2884
- throw codedError(
2885
- 'E_PUBLISH',
2886
- `Publisher returned invalid publication roots: ${safeMessage(error)}`,
2887
- );
2888
- }
2889
- return {
2890
- manifest,
2891
- catalogArtifact: {
2892
- relativePath: initiativeCatalogPath(manifest.slug),
2893
- hash: hashBytes(Buffer.from(serializeInitiativeCatalog(catalog))),
2894
- },
2895
- };
2896
- }
2897
-
2898
- async function parseCli(argv) {
2899
- let requestPath;
2900
- const options = {};
2901
- for (let index = 0; index < argv.length; index += 1) {
2902
- const value = argv[index];
2903
- if (value === '--request') {
2904
- requestPath = argv[++index];
2905
- } else if (value === '--reviewed-source') {
2906
- const path = argv[++index];
2907
- if (!path) throw new Error('--reviewed-source requires a JSON path.');
2908
- options.reviewedSource = JSON.parse(await readFile(path, 'utf8'));
2909
- } else if (
2910
- [
2911
- '--author-module',
2912
- '--critic-module',
2913
- '--publish-module',
2914
- '--durability-module',
2915
- ].includes(value)
2916
- ) {
2917
- const path = argv[++index];
2918
- if (!path) throw new Error(`${value} requires a module path.`);
2919
- const loaded = await import(pathToFileURL(resolve(path)).href);
2920
- const key = value.slice(2, -'-module'.length);
2921
- options[key] = loaded.default ?? loaded[key];
2922
- if (typeof options[key] !== 'function') {
2923
- throw new Error(`${value} must export a callback function.`);
2924
- }
2925
- } else {
2926
- throw new Error(`Unknown option: ${value}.`);
2927
- }
2928
- }
2929
- if (!requestPath) {
2930
- throw new Error(
2931
- 'Usage: run.mjs --request <json> [--reviewed-source <json>] [--author-module <mjs>] [--critic-module <mjs>] [--publish-module <mjs>] [--durability-module <mjs>]',
2932
- );
2933
- }
2934
- return { requestPath, options };
2935
- }
2936
-
2937
- function hashBytes(value) {
2938
- return `sha256:${createHash('sha256').update(value).digest('hex')}`;
2939
- }
2940
-
2941
- function safeId(value) {
2942
- return value.replaceAll(/[^a-zA-Z0-9._-]/g, '-');
2943
- }
2944
-
2945
- function slugify(value) {
2946
- return value
2947
- .toLowerCase()
2948
- .trim()
2949
- .replaceAll(/[^a-z0-9]+/g, '-')
2950
- .replaceAll(/^-+|-+$/g, '');
2951
- }
2952
-
2953
- function humanize(value) {
2954
- return value
2955
- .split('-')
2956
- .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
2957
- .join(' ');
2958
- }
2959
-
2960
- function codedError(code, message) {
2961
- const error = new Error(message);
2962
- error.code = code;
2963
- return error;
2964
- }
2965
-
2966
- /**
2967
- * Terminal evidence reasons are unique by (stage, kind), so a second reason for
2968
- * a pair that is already present folds into its count rather than appending a
2969
- * duplicate the closed contract would reject.
2970
- */
2971
- function mergeTerminalReasons(reasons, addition) {
2972
- const existing = reasons.find(
2973
- ({ stage, kind }) => stage === addition.stage && kind === addition.kind,
2974
- );
2975
- if (!existing) return [...reasons, addition];
2976
- return reasons.map((entry) =>
2977
- entry === existing
2978
- ? { ...entry, count: (entry.count ?? 1) + (addition.count ?? 1) }
2979
- : entry,
2980
- );
2981
- }
2982
-
2983
- function withEvidenceReason(error, stage, kind) {
2984
- error.evidenceReason = evidenceReason(stage, kind);
2985
- LOCALLY_PROJECTED_ERRORS.add(error);
2986
- return error;
2987
- }
2988
-
2989
- function localEvidenceReason(error) {
2990
- return error !== null &&
2991
- (typeof error === 'object' || typeof error === 'function') &&
2992
- LOCALLY_PROJECTED_ERRORS.has(error)
2993
- ? error.evidenceReason
2994
- : null;
2995
- }
2996
-
2997
- function evidenceStageForBuildStage(stage) {
2998
- return (
2999
- {
3000
- validate: 'planning',
3001
- 'fact-base': 'planning',
3002
- content: 'authoring',
3003
- theme: 'authoring',
3004
- render: 'rendering',
3005
- qa: 'browser-review',
3006
- durability: 'durability',
3007
- publish: 'durability',
3008
- }[stage] ?? 'finalization'
3009
- );
3010
- }
3011
-
3012
- function stageErrorCode(stage) {
3013
- return `E_${stage.toUpperCase().replaceAll('-', '_')}`;
3014
- }
3015
-
3016
- function safeMessage(_error) {
3017
- return 'Operation failed.';
3018
- }
3019
-
3020
- function retainedWarnings(warnings) {
3021
- return [...new Set(warnings.filter(isLocalWarningCode))];
3022
- }
3023
-
3024
- function isLocalWarningCode(value) {
3025
- return (
3026
- typeof value === 'string' &&
3027
- /^(?:fact-base-freshness-warning|theme-selection-normalized|durability-evidence-required|publish-receipt-evidence-required|(?:expansion|guideline|render|qa)-[a-z0-9-]+|visual-review-required:[a-z0-9-]+|stage-reopened:[a-z0-9-]+:[0-9TZ:.-]+|missing-(?:theme-token|required-anchor):[a-z0-9-]+)$/.test(
3028
- value,
3029
- )
3030
- );
3031
- }
3032
-
3033
- if (
3034
- process.argv[1] &&
3035
- pathToFileURL(process.argv[1]).href === import.meta.url
3036
- ) {
3037
- process.exitCode = await runExplainerCli();
3038
- }