@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.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- 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
|
-
}
|