@open-agent-toolkit/cli 0.2.20 → 0.2.21
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/docs/workflows/projects/artifacts.md +1 -1
- package/assets/docs/workflows/skills/explainer-kit.md +188 -23
- package/assets/docs/workflows/skills/index.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +87 -25
- package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
- package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
- package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
- package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
- package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
- package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
- package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
- package/assets/skills/explainer-kit/references/contracts.md +45 -19
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
- package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +680 -0
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +309 -10
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +313 -41
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +147 -2
- package/assets/skills/explainer-kit/scripts/run.mjs +796 -272
- package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
- package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
- package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
- package/assets/skills/explainer-kit/templates/house-style.html +82 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
- package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
- package/assets/skills/oat-project-complete/SKILL.md +18 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
- package/assets/skills/oat-wave-execute/SKILL.md +12 -19
- package/assets/skills/oat-wave-program/SKILL.md +12 -13
- package/package.json +2 -2
- package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
- package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
|
@@ -9,21 +9,37 @@ import { resolveContentApproval } from './lib/content-approval.mjs';
|
|
|
9
9
|
import { canonicalHash, validateContract } from './lib/contracts.mjs';
|
|
10
10
|
import { processFactBase } from './lib/fact-base.mjs';
|
|
11
11
|
import { writeJsonAtomic, writeTextAtomic } from './lib/fs-safe.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { validateHtmlSafety } from './lib/html-safety.mjs';
|
|
13
13
|
import {
|
|
14
|
+
auditArtifactSet,
|
|
15
|
+
checkGuidelines,
|
|
16
|
+
checkSourceDumping,
|
|
17
|
+
RENDER_QA_WARNING_IDS,
|
|
18
|
+
renderQaWarningIds,
|
|
19
|
+
renderWarningIds,
|
|
20
|
+
} from './lib/qa.mjs';
|
|
21
|
+
import {
|
|
22
|
+
evaluateExpansionProposals,
|
|
14
23
|
loadRecipe,
|
|
24
|
+
recipeExpansion,
|
|
25
|
+
recipeFloor,
|
|
26
|
+
recipeRequiredNarrative,
|
|
15
27
|
shouldStopDiscovery,
|
|
16
28
|
validateContentModel,
|
|
17
29
|
validateSourceBindings,
|
|
18
30
|
} from './lib/recipes.mjs';
|
|
19
31
|
import {
|
|
20
32
|
initializeRun,
|
|
33
|
+
reopenBuildStages,
|
|
21
34
|
updateBuildRecord,
|
|
22
35
|
writeManifestAtomic,
|
|
23
36
|
} from './lib/records.mjs';
|
|
24
|
-
import { renderArtifact } from './lib/render.mjs';
|
|
37
|
+
import { artifactPath, renderArtifact } from './lib/render.mjs';
|
|
25
38
|
import { resolveTheme } from './lib/theme.mjs';
|
|
26
39
|
|
|
40
|
+
// Stages a rejected draft reruns once its content is corrected.
|
|
41
|
+
const REOPENED_ON_REJECTION = Object.freeze(['render', 'qa']);
|
|
42
|
+
|
|
27
43
|
export async function runExplainer(request, options = {}) {
|
|
28
44
|
assertValidRequest(request);
|
|
29
45
|
const recipe = loadRecipe(request.recipe.id, request.recipe.version);
|
|
@@ -39,13 +55,26 @@ export async function runExplainer(request, options = {}) {
|
|
|
39
55
|
contentModels: [],
|
|
40
56
|
contentPaths: new Map(),
|
|
41
57
|
authorResultPaths: [],
|
|
58
|
+
resolvedArtifacts: [],
|
|
59
|
+
authoredContent: new Map(),
|
|
60
|
+
expansion: {
|
|
61
|
+
valid: true,
|
|
62
|
+
accepted: [],
|
|
63
|
+
rejected: [],
|
|
64
|
+
warnings: [],
|
|
65
|
+
errors: [],
|
|
66
|
+
},
|
|
67
|
+
qaErrors: [],
|
|
42
68
|
theme: null,
|
|
69
|
+
themeWarnings: [],
|
|
43
70
|
renderStrategy: run.request.theme.renderStrategy,
|
|
44
71
|
rendered: [],
|
|
45
72
|
artifacts: [],
|
|
46
73
|
warnings: [],
|
|
74
|
+
reopenedWarnings: {},
|
|
47
75
|
discovery: { rounds: 0, findings: [], reason: 'not-requested' },
|
|
48
76
|
approval: null,
|
|
77
|
+
resumedApprovalStatus: null,
|
|
49
78
|
};
|
|
50
79
|
|
|
51
80
|
try {
|
|
@@ -82,114 +111,54 @@ export async function runExplainer(request, options = {}) {
|
|
|
82
111
|
status: processed.checks.warnings.length > 0 ? 'warned' : 'passed',
|
|
83
112
|
};
|
|
84
113
|
});
|
|
114
|
+
await prepareTheme(state);
|
|
85
115
|
await executeStage(run, 'content', options, async () => {
|
|
86
116
|
state.discovery = await runDiscovery(recipe, state.factBase, options);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
for (const model of state.contentModels) {
|
|
97
|
-
const validation = validateContentModel(recipe, model);
|
|
98
|
-
if (!validation.valid) {
|
|
99
|
-
throw codedError(
|
|
100
|
-
'E_CONTENT',
|
|
101
|
-
`Invalid content model: ${validation.errors.join('; ')}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const path = `source/content/${model.artifactId}.md`;
|
|
105
|
-
await writeTextAtomic(run.runRoot, path, contentMarkdown(model));
|
|
106
|
-
state.contentPaths.set(model.artifactId, path);
|
|
107
|
-
}
|
|
108
|
-
return { outputPaths: [...state.contentPaths.values()] };
|
|
117
|
+
await createAuthoredContent(state, options, now);
|
|
118
|
+
return {
|
|
119
|
+
outputPaths: [
|
|
120
|
+
...state.contentPaths.values(),
|
|
121
|
+
...state.authorResultPaths,
|
|
122
|
+
],
|
|
123
|
+
warnings: state.expansion.warnings,
|
|
124
|
+
status: state.expansion.warnings.length > 0 ? 'warned' : 'passed',
|
|
125
|
+
};
|
|
109
126
|
});
|
|
110
127
|
}
|
|
111
128
|
|
|
129
|
+
if (!resumed) {
|
|
130
|
+
await executeThemeStage(state, options);
|
|
131
|
+
}
|
|
132
|
+
if (!resumed || state.resumedApprovalStatus === 'rejected') {
|
|
133
|
+
if (state.resumedApprovalStatus === 'rejected') {
|
|
134
|
+
const reopened = await reopenBuildStages(run, {
|
|
135
|
+
ids: [...REOPENED_ON_REJECTION],
|
|
136
|
+
reason: 'content-rejected',
|
|
137
|
+
});
|
|
138
|
+
// Reopen markers are the D4 audit trail, so they survive the rerun's
|
|
139
|
+
// replacement of each stage's warning set.
|
|
140
|
+
state.reopenedWarnings = Object.fromEntries(
|
|
141
|
+
reopened.stages.map(({ id, warnings }) => [
|
|
142
|
+
id,
|
|
143
|
+
warnings.filter((warning) => warning.startsWith('stage-reopened:')),
|
|
144
|
+
]),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
await executeRenderStage(state, options);
|
|
148
|
+
await executeQaStage(state, options);
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
state.approval = await resolveContentApproval(
|
|
113
152
|
run,
|
|
114
153
|
run.request.mode,
|
|
115
154
|
options.reviewedSource,
|
|
116
155
|
state.authorResultPaths,
|
|
156
|
+
approvalArtifacts(state),
|
|
117
157
|
);
|
|
118
158
|
if (!state.approval.canResume) {
|
|
119
159
|
return resultFor(state);
|
|
120
160
|
}
|
|
121
161
|
|
|
122
|
-
await executeStage(run, 'theme', options, async () => {
|
|
123
|
-
const resolved = await resolveTheme(run.request.theme);
|
|
124
|
-
state.theme = resolved.theme;
|
|
125
|
-
state.renderStrategy = resolved.renderStrategy;
|
|
126
|
-
state.warnings.push(...resolved.warnings);
|
|
127
|
-
await writeJsonAtomic(run.runRoot, 'theme.resolved.json', state.theme);
|
|
128
|
-
return {
|
|
129
|
-
outputPaths: ['theme.resolved.json'],
|
|
130
|
-
warnings: resolved.warnings,
|
|
131
|
-
status: resolved.warnings.length > 0 ? 'warned' : 'passed',
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
await executeStage(run, 'render', options, async () => {
|
|
135
|
-
for (const recipeArtifact of recipe.artifacts) {
|
|
136
|
-
const content = state.contentModels.find(
|
|
137
|
-
({ artifactId }) => artifactId === recipeArtifact.id,
|
|
138
|
-
);
|
|
139
|
-
const rendered = await renderArtifact({
|
|
140
|
-
recipeArtifact,
|
|
141
|
-
content,
|
|
142
|
-
theme: state.theme,
|
|
143
|
-
renderStrategy: state.renderStrategy,
|
|
144
|
-
...(run.request.publicBaseUrl && {
|
|
145
|
-
publicBaseUrl: run.request.publicBaseUrl,
|
|
146
|
-
}),
|
|
147
|
-
});
|
|
148
|
-
await writeTextAtomic(
|
|
149
|
-
run.runRoot,
|
|
150
|
-
rendered.renderedPath,
|
|
151
|
-
rendered.html,
|
|
152
|
-
);
|
|
153
|
-
state.rendered.push(rendered);
|
|
154
|
-
state.artifacts.push({
|
|
155
|
-
id: rendered.artifactId,
|
|
156
|
-
type: rendered.type,
|
|
157
|
-
contentPath: state.contentPaths.get(rendered.artifactId),
|
|
158
|
-
renderedPath: rendered.renderedPath,
|
|
159
|
-
mediaType: rendered.mediaType,
|
|
160
|
-
status: 'built',
|
|
161
|
-
hash: hashBytes(rendered.html),
|
|
162
|
-
rebuildable: false,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
outputPaths: state.rendered.map(({ renderedPath }) => renderedPath),
|
|
167
|
-
};
|
|
168
|
-
});
|
|
169
|
-
await executeStage(run, 'qa', options, async () => {
|
|
170
|
-
const report = await auditArtifactSet({
|
|
171
|
-
artifacts: state.rendered.map((artifact) => ({
|
|
172
|
-
id: artifact.artifactId,
|
|
173
|
-
type: artifact.type,
|
|
174
|
-
html: artifact.html,
|
|
175
|
-
})),
|
|
176
|
-
...(options.denylist && { denylist: options.denylist }),
|
|
177
|
-
...(options.browserProbe && { browserProbe: options.browserProbe }),
|
|
178
|
-
...(options.widths && { widths: options.widths }),
|
|
179
|
-
});
|
|
180
|
-
if (!report.valid) {
|
|
181
|
-
throw codedError(
|
|
182
|
-
'E_QA',
|
|
183
|
-
report.issues
|
|
184
|
-
.map(({ code, message }) => `${code}: ${message}`)
|
|
185
|
-
.join('; '),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
return {
|
|
189
|
-
outputPaths: state.rendered.map(({ renderedPath }) => renderedPath),
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
|
|
193
162
|
await executeDurabilityAndPublish(state, options, now);
|
|
194
163
|
await persistManifest(state, now());
|
|
195
164
|
return resultFor(state);
|
|
@@ -201,6 +170,202 @@ export async function runExplainer(request, options = {}) {
|
|
|
201
170
|
}
|
|
202
171
|
}
|
|
203
172
|
|
|
173
|
+
async function executeThemeStage(state, options) {
|
|
174
|
+
await executeStage(state.run, 'theme', options, async () => {
|
|
175
|
+
await writeJsonAtomic(
|
|
176
|
+
state.run.runRoot,
|
|
177
|
+
'theme.resolved.json',
|
|
178
|
+
state.theme,
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
outputPaths: ['theme.resolved.json'],
|
|
182
|
+
warnings: state.themeWarnings,
|
|
183
|
+
status: state.themeWarnings.length > 0 ? 'warned' : 'passed',
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function prepareTheme(state) {
|
|
189
|
+
const resolved = await resolveTheme(state.run.request.theme);
|
|
190
|
+
state.theme = resolved.theme;
|
|
191
|
+
state.renderStrategy = resolved.renderStrategy;
|
|
192
|
+
state.themeWarnings = resolved.warnings;
|
|
193
|
+
state.warnings.push(...resolved.warnings);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function executeRenderStage(state, options) {
|
|
197
|
+
state.rendered = [];
|
|
198
|
+
state.artifacts = [];
|
|
199
|
+
await executeStage(state.run, 'render', options, async () => {
|
|
200
|
+
for (const artifact of state.resolvedArtifacts) {
|
|
201
|
+
const rendered =
|
|
202
|
+
artifact.authoring === 'markdown'
|
|
203
|
+
? await renderArtifact({
|
|
204
|
+
recipeArtifact: renderDescriptor(artifact),
|
|
205
|
+
content: state.contentModels.find(
|
|
206
|
+
({ artifactId }) => artifactId === artifact.id,
|
|
207
|
+
),
|
|
208
|
+
theme: state.theme,
|
|
209
|
+
renderStrategy: state.renderStrategy,
|
|
210
|
+
...(state.run.request.publicBaseUrl && {
|
|
211
|
+
publicBaseUrl: state.run.request.publicBaseUrl,
|
|
212
|
+
}),
|
|
213
|
+
})
|
|
214
|
+
: artisticRender(state, artifact);
|
|
215
|
+
await writeTextAtomic(
|
|
216
|
+
state.run.runRoot,
|
|
217
|
+
rendered.renderedPath,
|
|
218
|
+
rendered.html,
|
|
219
|
+
);
|
|
220
|
+
state.rendered.push(rendered);
|
|
221
|
+
state.artifacts.push(artifactRecord(state, rendered));
|
|
222
|
+
}
|
|
223
|
+
// D7 degradation findings are guideline severity, so they travel to the
|
|
224
|
+
// run result and the manifest as warnings rather than failing the stage.
|
|
225
|
+
const degradation = renderWarningIds(
|
|
226
|
+
state.rendered.flatMap(
|
|
227
|
+
({ warnings: sectionWarnings = [] }) => sectionWarnings,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
state.warnings.push(...degradation);
|
|
231
|
+
const warnings = [...(state.reopenedWarnings.render ?? []), ...degradation];
|
|
232
|
+
return {
|
|
233
|
+
outputPaths: state.rendered.map(({ renderedPath }) => renderedPath),
|
|
234
|
+
warnings,
|
|
235
|
+
status: warnings.length > 0 ? 'warned' : 'passed',
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function executeQaStage(state, options) {
|
|
241
|
+
await executeStage(state.run, 'qa', options, async () => {
|
|
242
|
+
const htmlSafetyErrors = [];
|
|
243
|
+
const qaWarnings = [];
|
|
244
|
+
return auditRenderedArtifacts(state, options, {
|
|
245
|
+
browserProbe: resolveBrowserProbe(options),
|
|
246
|
+
htmlSafetyErrors,
|
|
247
|
+
qaWarnings,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Render QA drives a caller-supplied probe only. The core never launches a
|
|
254
|
+
* headless runtime itself; agents review rendered output in their own browser.
|
|
255
|
+
*/
|
|
256
|
+
function resolveBrowserProbe(options) {
|
|
257
|
+
if (options.browserProbe === undefined) return null;
|
|
258
|
+
if (typeof options.browserProbe !== 'function') {
|
|
259
|
+
throw codedError(
|
|
260
|
+
'E_BROWSER_PROBE',
|
|
261
|
+
'options.browserProbe must be a function when supplied.',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return options.browserProbe;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function auditRenderedArtifacts(
|
|
268
|
+
state,
|
|
269
|
+
options,
|
|
270
|
+
{ browserProbe, htmlSafetyErrors, qaWarnings },
|
|
271
|
+
) {
|
|
272
|
+
for (const artifact of state.resolvedArtifacts.filter(
|
|
273
|
+
({ authoring }) => authoring === 'html',
|
|
274
|
+
)) {
|
|
275
|
+
const safety = validateHtmlSafety({
|
|
276
|
+
html: state.authoredContent.get(artifact.id),
|
|
277
|
+
shell: artifact.shellContent,
|
|
278
|
+
shellName: artifact.shell ?? artifact.template,
|
|
279
|
+
});
|
|
280
|
+
htmlSafetyErrors.push(
|
|
281
|
+
...safety.errors.map((code) => ({
|
|
282
|
+
code,
|
|
283
|
+
message: `Artistic artifact ${artifact.id} failed DOM safety validation.`,
|
|
284
|
+
})),
|
|
285
|
+
);
|
|
286
|
+
qaWarnings.push(...safety.warnings);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const probeArtifacts = state.rendered.map((artifact) => ({
|
|
290
|
+
id: artifact.artifactId,
|
|
291
|
+
type: artifact.type,
|
|
292
|
+
html: artifact.html,
|
|
293
|
+
}));
|
|
294
|
+
const report = await auditArtifactSet({
|
|
295
|
+
artifacts: probeArtifacts,
|
|
296
|
+
...(options.denylist && { denylist: options.denylist }),
|
|
297
|
+
...(browserProbe && { browserProbe }),
|
|
298
|
+
...(options.widths && { widths: options.widths }),
|
|
299
|
+
});
|
|
300
|
+
const hardIssues = report.issues.filter((issue) => isHardQaIssue(issue.code));
|
|
301
|
+
const warningIssues = report.issues.filter(
|
|
302
|
+
(issue) => !isHardQaIssue(issue.code),
|
|
303
|
+
);
|
|
304
|
+
// A code the render-QA vocabulary already covers must not also emit an ad
|
|
305
|
+
// hoc `qa-*` twin; the generic conversion is for structural codes only.
|
|
306
|
+
qaWarnings.push(
|
|
307
|
+
...renderQaWarningIds(warningIssues),
|
|
308
|
+
...warningIssues
|
|
309
|
+
.filter(({ code }) => renderQaWarningIds([{ code }]).length === 0)
|
|
310
|
+
.map(({ code }) => `qa-${code}`),
|
|
311
|
+
);
|
|
312
|
+
if (!browserProbe) {
|
|
313
|
+
qaWarnings.push(RENDER_QA_WARNING_IDS.skippedNoProbe);
|
|
314
|
+
}
|
|
315
|
+
const guidelines = checkGuidelines({
|
|
316
|
+
recipe: state.recipe,
|
|
317
|
+
artifacts: probeArtifacts,
|
|
318
|
+
expansion: state.expansion,
|
|
319
|
+
});
|
|
320
|
+
qaWarnings.push(...guidelines.warnings, ...state.expansion.warnings);
|
|
321
|
+
|
|
322
|
+
const errors = [...state.qaErrors, ...htmlSafetyErrors, ...hardIssues];
|
|
323
|
+
if (errors.length > 0) {
|
|
324
|
+
throw codedError(
|
|
325
|
+
'E_QA',
|
|
326
|
+
errors.map(({ code, message }) => `${code}: ${message}`).join('; '),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
state.warnings.push(...qaWarnings);
|
|
330
|
+
const warnings = [
|
|
331
|
+
...(state.reopenedWarnings.qa ?? []),
|
|
332
|
+
...new Set(qaWarnings),
|
|
333
|
+
];
|
|
334
|
+
return {
|
|
335
|
+
outputPaths: state.rendered.map(({ renderedPath }) => renderedPath),
|
|
336
|
+
warnings,
|
|
337
|
+
status: warnings.length > 0 ? 'warned' : 'passed',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isHardQaIssue(code) {
|
|
342
|
+
return (
|
|
343
|
+
[
|
|
344
|
+
'denylisted-string',
|
|
345
|
+
'external-asset',
|
|
346
|
+
'link-form',
|
|
347
|
+
'tag-balance',
|
|
348
|
+
'unresolved-token',
|
|
349
|
+
].includes(code) || code.startsWith('cohesion-')
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function artisticRender(state, artifact) {
|
|
354
|
+
const renderedPath = artifactPath(renderDescriptor(artifact), state.run.slug);
|
|
355
|
+
const publicBaseUrl = state.run.request.publicBaseUrl?.replace(/\/+$/g, '');
|
|
356
|
+
return {
|
|
357
|
+
artifactId: artifact.id,
|
|
358
|
+
type: artifact.type,
|
|
359
|
+
renderedPath,
|
|
360
|
+
publicUrl: publicBaseUrl
|
|
361
|
+
? `${publicBaseUrl}/${renderedPath.slice('site/'.length)}`
|
|
362
|
+
: undefined,
|
|
363
|
+
mediaType: 'text/html',
|
|
364
|
+
html: state.authoredContent.get(artifact.id),
|
|
365
|
+
warnings: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
204
369
|
async function loadResumableRun(request) {
|
|
205
370
|
const normalized = structuredClone(request);
|
|
206
371
|
normalized.theme = {
|
|
@@ -222,11 +387,16 @@ async function loadResumableRun(request) {
|
|
|
222
387
|
throw error;
|
|
223
388
|
}
|
|
224
389
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
390
|
+
const approvalUnresolved = ['pending', 'rejected'].includes(approval.status);
|
|
391
|
+
const completedBeforeApproval = ['content', 'theme', 'render', 'qa'].every(
|
|
392
|
+
(id) =>
|
|
393
|
+
['passed', 'warned', 'skipped'].includes(
|
|
394
|
+
record.stages?.find((stage) => stage.id === id)?.status,
|
|
395
|
+
),
|
|
396
|
+
);
|
|
227
397
|
if (
|
|
228
|
-
|
|
229
|
-
|
|
398
|
+
!approvalUnresolved ||
|
|
399
|
+
!completedBeforeApproval ||
|
|
230
400
|
approval.runId !== record.runId
|
|
231
401
|
) {
|
|
232
402
|
return null;
|
|
@@ -259,39 +429,119 @@ async function loadResumableRun(request) {
|
|
|
259
429
|
}
|
|
260
430
|
|
|
261
431
|
async function hydrateResumableState(state) {
|
|
262
|
-
const [factBase, approval] = await Promise.all([
|
|
432
|
+
const [factBase, approval, theme, record] = await Promise.all([
|
|
263
433
|
readJson(join(state.run.runRoot, 'source/fact-base.json')),
|
|
264
434
|
readJson(join(state.run.runRoot, 'source/content-approval.json')),
|
|
435
|
+
readJson(join(state.run.runRoot, 'theme.resolved.json')),
|
|
436
|
+
readJson(state.run.buildRecordPath),
|
|
265
437
|
]);
|
|
266
438
|
state.factBase = factBase;
|
|
267
|
-
state.
|
|
268
|
-
|
|
269
|
-
|
|
439
|
+
state.theme = theme;
|
|
440
|
+
state.themeWarnings = [];
|
|
441
|
+
state.resumedApprovalStatus = approval.status;
|
|
442
|
+
// Reopened stages rerun against the corrected content, so carrying their
|
|
443
|
+
// prior warnings forward would outlive the fix that resolved them.
|
|
444
|
+
const rerunning =
|
|
445
|
+
approval.status === 'rejected' ? REOPENED_ON_REJECTION : [];
|
|
446
|
+
state.warnings.push(
|
|
447
|
+
...record.stages
|
|
448
|
+
.filter(({ id }) => !rerunning.includes(id))
|
|
449
|
+
.flatMap(({ warnings = [] }) =>
|
|
450
|
+
warnings.filter((warning) => !warning.startsWith('stage-reopened:')),
|
|
451
|
+
),
|
|
452
|
+
);
|
|
270
453
|
state.inputHashes = inputHashes(state.factBase);
|
|
271
454
|
state.factBaseHash = canonicalHash(state.factBase);
|
|
272
455
|
state.contentModels = [];
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (!validation.valid) {
|
|
287
|
-
throw codedError(
|
|
288
|
-
'E_CONTENT',
|
|
289
|
-
`Reviewed content is invalid: ${validation.errors.join('; ')}`,
|
|
456
|
+
const persistedArtifacts = Array.isArray(approval.artifacts)
|
|
457
|
+
? approval.artifacts
|
|
458
|
+
: recipeFloor(state.recipe).map((artifact) => ({
|
|
459
|
+
artifactId: artifact.id,
|
|
460
|
+
origin: 'floor',
|
|
461
|
+
authoring: artifact.authoring ?? 'markdown',
|
|
462
|
+
contentPath: `source/content/${artifact.id}.md`,
|
|
463
|
+
}));
|
|
464
|
+
for (const persisted of persistedArtifacts) {
|
|
465
|
+
const artifact = resolvedArtifactFromApproval(state.recipe, persisted);
|
|
466
|
+
if (artifact.authoring === 'html') {
|
|
467
|
+
artifact.shellContent = await readSkillFile(
|
|
468
|
+
`templates/${artifact.shell}.html`,
|
|
290
469
|
);
|
|
291
470
|
}
|
|
292
|
-
|
|
293
|
-
|
|
471
|
+
const content = await readFile(
|
|
472
|
+
join(state.run.runRoot, persisted.contentPath),
|
|
473
|
+
'utf8',
|
|
474
|
+
);
|
|
475
|
+
state.resolvedArtifacts.push(artifact);
|
|
476
|
+
state.authoredContent.set(artifact.id, content);
|
|
477
|
+
state.contentPaths.set(artifact.id, persisted.contentPath);
|
|
478
|
+
if (persisted.authorResultPath) {
|
|
479
|
+
state.authorResultPaths.push(persisted.authorResultPath);
|
|
480
|
+
}
|
|
294
481
|
}
|
|
482
|
+
const links = expansionLinks(state.resolvedArtifacts);
|
|
483
|
+
for (const artifact of state.resolvedArtifacts) {
|
|
484
|
+
if (artifact.authoring !== 'markdown') continue;
|
|
485
|
+
state.contentModels.push(
|
|
486
|
+
markdownContentModel(
|
|
487
|
+
artifact,
|
|
488
|
+
state.run.slug,
|
|
489
|
+
state.authoredContent.get(artifact.id),
|
|
490
|
+
artifact.origin === 'floor' ? links : [],
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
state.expansion.accepted = state.resolvedArtifacts
|
|
495
|
+
.filter(({ origin }) => origin === 'expansion')
|
|
496
|
+
.map((artifact) => ({
|
|
497
|
+
id: artifact.id,
|
|
498
|
+
profileId: artifact.profileId,
|
|
499
|
+
rationale: 'Persisted approved expansion artifact.',
|
|
500
|
+
status: 'accepted',
|
|
501
|
+
profile: expansionProfile(state.recipe, artifact.profileId),
|
|
502
|
+
}));
|
|
503
|
+
await hydrateRenderedState(state);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function hydrateRenderedState(state) {
|
|
507
|
+
state.rendered = [];
|
|
508
|
+
state.artifacts = [];
|
|
509
|
+
for (const artifact of state.resolvedArtifacts) {
|
|
510
|
+
const descriptor =
|
|
511
|
+
artifact.authoring === 'markdown'
|
|
512
|
+
? await renderArtifact({
|
|
513
|
+
recipeArtifact: renderDescriptor(artifact),
|
|
514
|
+
content: state.contentModels.find(
|
|
515
|
+
({ artifactId }) => artifactId === artifact.id,
|
|
516
|
+
),
|
|
517
|
+
theme: state.theme,
|
|
518
|
+
renderStrategy: state.renderStrategy,
|
|
519
|
+
...(state.run.request.publicBaseUrl && {
|
|
520
|
+
publicBaseUrl: state.run.request.publicBaseUrl,
|
|
521
|
+
}),
|
|
522
|
+
})
|
|
523
|
+
: artisticRender(state, artifact);
|
|
524
|
+
const html = await readFile(
|
|
525
|
+
join(state.run.runRoot, descriptor.renderedPath),
|
|
526
|
+
'utf8',
|
|
527
|
+
);
|
|
528
|
+
const rendered = { ...descriptor, html };
|
|
529
|
+
state.rendered.push(rendered);
|
|
530
|
+
state.artifacts.push(artifactRecord(state, rendered));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function artifactRecord(state, rendered) {
|
|
535
|
+
return {
|
|
536
|
+
id: rendered.artifactId,
|
|
537
|
+
type: rendered.type,
|
|
538
|
+
contentPath: state.contentPaths.get(rendered.artifactId),
|
|
539
|
+
renderedPath: rendered.renderedPath,
|
|
540
|
+
mediaType: rendered.mediaType,
|
|
541
|
+
status: 'built',
|
|
542
|
+
hash: hashBytes(rendered.html),
|
|
543
|
+
rebuildable: false,
|
|
544
|
+
};
|
|
295
545
|
}
|
|
296
546
|
|
|
297
547
|
async function readJson(path) {
|
|
@@ -338,7 +588,7 @@ async function executeStage(run, id, options, operation) {
|
|
|
338
588
|
id,
|
|
339
589
|
status: result.status ?? 'passed',
|
|
340
590
|
outputPaths: result.outputPaths ?? [],
|
|
341
|
-
warnings: result.warnings
|
|
591
|
+
...(result.warnings !== undefined && { warnings: result.warnings }),
|
|
342
592
|
});
|
|
343
593
|
} catch (error) {
|
|
344
594
|
await updateBuildRecord(run, {
|
|
@@ -512,7 +762,7 @@ async function persistFailureManifest(state, error, createdAt) {
|
|
|
512
762
|
const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
|
|
513
763
|
const recordedIds = new Set(state.artifacts.map(({ id }) => id));
|
|
514
764
|
state.artifacts.push(
|
|
515
|
-
...state.recipe
|
|
765
|
+
...recipeFloor(state.recipe)
|
|
516
766
|
.filter(({ id }) => !recordedIds.has(id))
|
|
517
767
|
.map((artifact) => ({
|
|
518
768
|
id: artifact.id,
|
|
@@ -567,162 +817,457 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
|
|
|
567
817
|
};
|
|
568
818
|
}
|
|
569
819
|
|
|
570
|
-
async function createAuthoredContent(state,
|
|
820
|
+
async function createAuthoredContent(state, options, now) {
|
|
821
|
+
const author = options.author;
|
|
571
822
|
if (typeof author !== 'function') {
|
|
572
823
|
throw codedError(
|
|
573
824
|
'E_AUTHOR_REQUIRED',
|
|
574
|
-
'
|
|
825
|
+
'Explainer runs require an explicit author callback in both modes.',
|
|
575
826
|
);
|
|
576
827
|
}
|
|
828
|
+
const trust = authorTrustContext(options, now);
|
|
577
829
|
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
throw codedError(
|
|
603
|
-
'E_AUTHOR_REQUEST',
|
|
604
|
-
contractErrorMessage('author request', requestValidation.errors),
|
|
605
|
-
);
|
|
606
|
-
}
|
|
830
|
+
const floor = await Promise.all(
|
|
831
|
+
recipeFloor(state.recipe).map((artifact) =>
|
|
832
|
+
authorArtifact(
|
|
833
|
+
state,
|
|
834
|
+
{
|
|
835
|
+
...artifact,
|
|
836
|
+
origin: 'floor',
|
|
837
|
+
shell: artifact.authoring === 'html' ? artifact.template : undefined,
|
|
838
|
+
},
|
|
839
|
+
author,
|
|
840
|
+
trust,
|
|
841
|
+
),
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
const proposals = floor.flatMap(
|
|
845
|
+
({ result }) => result.proposedArtifacts ?? [],
|
|
846
|
+
);
|
|
847
|
+
state.expansion = evaluateExpansionProposals(state.recipe, proposals);
|
|
848
|
+
if (!state.expansion.valid) {
|
|
849
|
+
throw codedError(
|
|
850
|
+
'E_AUTHOR_RESULT',
|
|
851
|
+
`Invalid expansion proposals: ${state.expansion.errors.join('; ')}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
607
854
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
855
|
+
const expansions = [];
|
|
856
|
+
for (const accepted of state.expansion.accepted) {
|
|
857
|
+
const profile = accepted.profile;
|
|
858
|
+
const item = await authorArtifact(
|
|
859
|
+
state,
|
|
860
|
+
{
|
|
861
|
+
id: accepted.id,
|
|
862
|
+
type: profile.type,
|
|
863
|
+
authoring: profile.authoring,
|
|
864
|
+
briefRef: profile.briefRef,
|
|
865
|
+
shell: profile.shell,
|
|
866
|
+
template:
|
|
867
|
+
profile.authoring === 'markdown'
|
|
868
|
+
? templateForType(profile.type)
|
|
869
|
+
: profile.shell,
|
|
870
|
+
required: false,
|
|
871
|
+
origin: 'expansion',
|
|
872
|
+
profileId: profile.profileId,
|
|
873
|
+
},
|
|
874
|
+
author,
|
|
875
|
+
trust,
|
|
876
|
+
);
|
|
877
|
+
if ((item.result.proposedArtifacts ?? []).length > 0) {
|
|
624
878
|
throw codedError(
|
|
625
879
|
'E_AUTHOR_RESULT',
|
|
626
|
-
`
|
|
880
|
+
`Expansion artifact ${accepted.id} cannot propose nested artifacts.`,
|
|
627
881
|
);
|
|
628
882
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
883
|
+
expansions.push(item);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const authored = [...floor, ...expansions];
|
|
887
|
+
state.resolvedArtifacts = authored.map(({ artifact }) => artifact);
|
|
888
|
+
const links = expansionLinks(state.resolvedArtifacts);
|
|
889
|
+
for (const item of authored) {
|
|
890
|
+
await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
|
|
891
|
+
await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
|
|
892
|
+
state.authorResultPaths.push(item.resultPath);
|
|
893
|
+
state.contentPaths.set(item.artifact.id, item.contentPath);
|
|
894
|
+
state.authoredContent.set(item.artifact.id, item.content);
|
|
895
|
+
if (item.artifact.authoring === 'markdown') {
|
|
896
|
+
state.contentModels.push(
|
|
897
|
+
assertValidContentModel(
|
|
898
|
+
state.recipe,
|
|
899
|
+
markdownContentModel(
|
|
900
|
+
item.artifact,
|
|
901
|
+
state.run.slug,
|
|
902
|
+
item.content,
|
|
903
|
+
item.artifact.origin === 'floor' ? links : [],
|
|
904
|
+
),
|
|
905
|
+
item.artifact,
|
|
906
|
+
),
|
|
643
907
|
);
|
|
644
908
|
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
645
911
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
912
|
+
// Expansion artifacts are not recipe-floor entries, so only floor content models
|
|
913
|
+
// can be checked against the floor-scoped contract.
|
|
914
|
+
function assertValidContentModel(recipe, model, artifact) {
|
|
915
|
+
if (artifact.origin !== 'floor') return model;
|
|
916
|
+
const result = validateContentModel(recipe, model);
|
|
917
|
+
if (!result.valid) {
|
|
918
|
+
throw codedError(
|
|
919
|
+
'E_CONTENT_MODEL',
|
|
920
|
+
`Authored content for ${artifact.id} violates the narrative contract: ${result.errors.join('; ')}`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return model;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Provenance authenticity cannot come from the party being identified, so
|
|
927
|
+
// identity and method are bound to trusted caller configuration and the
|
|
928
|
+
// generation time is stamped from the run's injected clock.
|
|
929
|
+
function authorTrustContext(options, now) {
|
|
930
|
+
const generatedAt = now();
|
|
931
|
+
const declared = options.authorProvenance;
|
|
932
|
+
if (declared === undefined) return { generatedAt, bound: null };
|
|
933
|
+
const valid =
|
|
934
|
+
typeof declared === 'object' &&
|
|
935
|
+
declared !== null &&
|
|
936
|
+
!Array.isArray(declared) &&
|
|
937
|
+
typeof declared.authorId === 'string' &&
|
|
938
|
+
declared.authorId.length > 0 &&
|
|
939
|
+
(declared.method === undefined ||
|
|
940
|
+
(typeof declared.method === 'string' && declared.method.length > 0));
|
|
941
|
+
if (!valid) {
|
|
942
|
+
throw codedError(
|
|
943
|
+
'E_AUTHOR_PROVENANCE',
|
|
944
|
+
'Trusted author provenance requires a non-empty authorId and, when present, a non-empty method.',
|
|
945
|
+
);
|
|
664
946
|
}
|
|
947
|
+
return {
|
|
948
|
+
generatedAt,
|
|
949
|
+
bound: {
|
|
950
|
+
authorId: declared.authorId,
|
|
951
|
+
...(declared.method !== undefined && { method: declared.method }),
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
}
|
|
665
955
|
|
|
666
|
-
|
|
667
|
-
|
|
956
|
+
function resolveAuthorProvenance(claimed, trust, artifactId) {
|
|
957
|
+
if ('trust' in claimed) {
|
|
958
|
+
throw codedError(
|
|
959
|
+
'E_AUTHOR_PROVENANCE',
|
|
960
|
+
`Author result for ${artifactId} must not assert a provenance trust level; the core stamps it.`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (trust.bound === null) {
|
|
964
|
+
return {
|
|
965
|
+
...claimed,
|
|
966
|
+
generatedAt: trust.generatedAt,
|
|
967
|
+
trust: 'self-asserted',
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (
|
|
971
|
+
claimed.authorId !== trust.bound.authorId ||
|
|
972
|
+
(trust.bound.method !== undefined && claimed.method !== trust.bound.method)
|
|
973
|
+
) {
|
|
974
|
+
throw codedError(
|
|
975
|
+
'E_AUTHOR_PROVENANCE',
|
|
976
|
+
`Author result for ${artifactId} claims provenance that does not match the trusted caller context.`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
...trust.bound,
|
|
981
|
+
generatedAt: trust.generatedAt,
|
|
982
|
+
trust: 'caller-bound',
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function authorArtifact(state, artifact, author, trust) {
|
|
987
|
+
const brief = await readSkillFile(artifact.briefRef);
|
|
988
|
+
const shellContent =
|
|
989
|
+
artifact.authoring === 'html'
|
|
990
|
+
? await readSkillFile(`templates/${artifact.shell}.html`)
|
|
991
|
+
: undefined;
|
|
992
|
+
const resolvedArtifact = {
|
|
993
|
+
...artifact,
|
|
994
|
+
...(shellContent && { shellContent }),
|
|
995
|
+
};
|
|
996
|
+
const requiredNarrative =
|
|
997
|
+
artifact.origin === 'floor'
|
|
998
|
+
? recipeRequiredNarrative(state.recipe, artifact.id)
|
|
999
|
+
: [];
|
|
1000
|
+
const authorRequest = {
|
|
1001
|
+
schemaVersion: 'explainer-kit.author-request/v2',
|
|
1002
|
+
artifactId: artifact.id,
|
|
1003
|
+
artifactType: artifact.type,
|
|
1004
|
+
authoring: artifact.authoring,
|
|
1005
|
+
brief,
|
|
1006
|
+
factBase: structuredClone(state.factBase),
|
|
1007
|
+
...(shellContent && { shell: shellContent }),
|
|
1008
|
+
theme: structuredClone(state.theme),
|
|
1009
|
+
...(artifact.origin === 'floor' &&
|
|
1010
|
+
requiredNarrative.length > 0 && {
|
|
1011
|
+
floor: { requiredNarrative },
|
|
1012
|
+
}),
|
|
1013
|
+
};
|
|
1014
|
+
const requestValidation = validateContract(
|
|
1015
|
+
'author-request/v2',
|
|
1016
|
+
authorRequest,
|
|
1017
|
+
);
|
|
1018
|
+
if (!requestValidation.valid) {
|
|
1019
|
+
throw codedError(
|
|
1020
|
+
'E_AUTHOR_REQUEST',
|
|
1021
|
+
contractErrorMessage('author request', requestValidation.errors),
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const result = await author(structuredClone(authorRequest));
|
|
1026
|
+
const resultValidation = validateContract('author-result/v2', result);
|
|
1027
|
+
if (!resultValidation.valid) {
|
|
1028
|
+
throw codedError(
|
|
1029
|
+
'E_AUTHOR_RESULT',
|
|
1030
|
+
contractErrorMessage('author result', resultValidation.errors),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const content = result.content?.[artifact.authoring];
|
|
1034
|
+
if (result.artifactId !== artifact.id || typeof content !== 'string') {
|
|
1035
|
+
throw codedError(
|
|
1036
|
+
'E_AUTHOR_RESULT',
|
|
1037
|
+
`Author result for ${artifact.id} must match its identity and ${artifact.authoring} path.`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const retained = {
|
|
1041
|
+
...structuredClone(result),
|
|
1042
|
+
provenance: resolveAuthorProvenance(
|
|
1043
|
+
result.provenance,
|
|
1044
|
+
trust,
|
|
1045
|
+
artifact.id,
|
|
1046
|
+
),
|
|
1047
|
+
};
|
|
1048
|
+
const retainedValidation = validateContract('author-result/v2', retained);
|
|
1049
|
+
if (!retainedValidation.valid) {
|
|
1050
|
+
throw codedError(
|
|
1051
|
+
'E_AUTHOR_PROVENANCE',
|
|
1052
|
+
contractErrorMessage('retained author result', retainedValidation.errors),
|
|
1053
|
+
);
|
|
668
1054
|
}
|
|
1055
|
+
const dumpCheck = checkSourceDumping({
|
|
1056
|
+
authoredText: content,
|
|
1057
|
+
sourceTexts: [
|
|
1058
|
+
...state.factBase.claims,
|
|
1059
|
+
...state.factBase.unresolvedClaims,
|
|
1060
|
+
].map(({ text }) => text),
|
|
1061
|
+
});
|
|
1062
|
+
state.qaErrors.push(
|
|
1063
|
+
...dumpCheck.issues.map((issue) => ({
|
|
1064
|
+
code: issue.code,
|
|
1065
|
+
message: issue.message,
|
|
1066
|
+
})),
|
|
1067
|
+
);
|
|
1068
|
+
|
|
669
1069
|
return {
|
|
670
|
-
|
|
671
|
-
|
|
1070
|
+
artifact: resolvedArtifact,
|
|
1071
|
+
result: retained,
|
|
1072
|
+
resultPath: `source/author/${artifact.id}.json`,
|
|
1073
|
+
content,
|
|
1074
|
+
contentPath: `source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
|
|
672
1075
|
};
|
|
673
1076
|
}
|
|
674
1077
|
|
|
1078
|
+
async function readSkillFile(relativePath) {
|
|
1079
|
+
return readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
675
1082
|
function contractErrorMessage(label, errors) {
|
|
676
1083
|
return `Invalid ${label}: ${errors
|
|
677
1084
|
.map(({ path, message }) => `${path}: ${message}`)
|
|
678
1085
|
.join('; ')}`;
|
|
679
1086
|
}
|
|
680
1087
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1088
|
+
// renderArtifact validates an exact key set, so normalized v2 floor entries
|
|
1089
|
+
// must be narrowed before they reach it.
|
|
1090
|
+
function renderDescriptor(artifact) {
|
|
1091
|
+
return {
|
|
1092
|
+
id: artifact.id,
|
|
1093
|
+
type: artifact.type,
|
|
1094
|
+
template: artifact.template,
|
|
1095
|
+
required: artifact.required,
|
|
1096
|
+
origin: artifact.origin,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const LEAD_SECTION_IDS = ['overview', 'introduction', 'lead'];
|
|
1101
|
+
|
|
1102
|
+
function markdownContentModel(artifact, slug, markdown, artifactLinks) {
|
|
1103
|
+
const titleMatch = markdown.match(/^# (.+)$/m);
|
|
1104
|
+
const title = titleMatch?.[1]?.trim() ?? humanize(artifact.id);
|
|
1105
|
+
const headings = [...markdown.matchAll(/^## (.+)$/gm)];
|
|
1106
|
+
if (headings.length === 0) {
|
|
1107
|
+
return contentModel({
|
|
1108
|
+
artifact,
|
|
1109
|
+
slug,
|
|
1110
|
+
title,
|
|
1111
|
+
artifactLinks,
|
|
1112
|
+
sections: [{ id: 'overview', title: 'Overview', content: markdown }],
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Prose between the document title and the first `##` is authored content,
|
|
1117
|
+
// so it is carried as a leading section rather than silently dropped.
|
|
1118
|
+
const bodyStart = titleMatch
|
|
1119
|
+
? titleMatch.index + titleMatch[0].length
|
|
1120
|
+
: 0;
|
|
1121
|
+
const lead = markdown.slice(bodyStart, headings[0].index).trim();
|
|
1122
|
+
const authoredIds = new Set(
|
|
1123
|
+
headings.map((heading) => slugify(heading[1].trim())),
|
|
1124
|
+
);
|
|
1125
|
+
const sections = headings.map((heading, index) => ({
|
|
1126
|
+
id: slugify(heading[1].trim()),
|
|
1127
|
+
title: heading[1].trim(),
|
|
1128
|
+
content: markdown
|
|
1129
|
+
.slice(
|
|
1130
|
+
heading.index + heading[0].length,
|
|
1131
|
+
headings[index + 1]?.index ?? markdown.length,
|
|
1132
|
+
)
|
|
1133
|
+
.trim(),
|
|
1134
|
+
}));
|
|
1135
|
+
if (lead.length > 0) {
|
|
1136
|
+
const leadId =
|
|
1137
|
+
LEAD_SECTION_IDS.find((candidate) => !authoredIds.has(candidate)) ??
|
|
1138
|
+
'lead';
|
|
1139
|
+
sections.unshift({
|
|
1140
|
+
id: leadId,
|
|
1141
|
+
title: humanize(leadId),
|
|
1142
|
+
content: lead,
|
|
1143
|
+
});
|
|
701
1144
|
}
|
|
1145
|
+
|
|
1146
|
+
return contentModel({
|
|
1147
|
+
artifact,
|
|
1148
|
+
slug,
|
|
1149
|
+
title,
|
|
1150
|
+
artifactLinks,
|
|
1151
|
+
sections: disambiguateSectionIds(sections),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// A repeated heading is legitimate authoring, but duplicate anchors break
|
|
1156
|
+
// navigation, so later collisions get a deterministic suffix.
|
|
1157
|
+
function disambiguateSectionIds(sections) {
|
|
1158
|
+
const used = new Map();
|
|
1159
|
+
return sections.map((section) => {
|
|
1160
|
+
const seen = used.get(section.id) ?? 0;
|
|
1161
|
+
used.set(section.id, seen + 1);
|
|
1162
|
+
if (seen === 0) return section;
|
|
1163
|
+
let candidate = `${section.id}-${seen + 1}`;
|
|
1164
|
+
let offset = seen + 1;
|
|
1165
|
+
while (used.has(candidate)) {
|
|
1166
|
+
offset += 1;
|
|
1167
|
+
candidate = `${section.id}-${offset}`;
|
|
1168
|
+
}
|
|
1169
|
+
used.set(candidate, 1);
|
|
1170
|
+
return { ...section, id: candidate };
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function contentModel({ artifact, slug, title, artifactLinks, sections }) {
|
|
702
1175
|
return {
|
|
703
1176
|
artifactId: artifact.id,
|
|
704
1177
|
slug,
|
|
705
|
-
title
|
|
706
|
-
description: `
|
|
1178
|
+
title,
|
|
1179
|
+
description: `Authored ${humanize(artifact.id).toLowerCase()}.`,
|
|
707
1180
|
eyebrow: 'Explainer Kit',
|
|
708
|
-
footer: '
|
|
709
|
-
sections
|
|
710
|
-
|
|
711
|
-
.filter(({ sections }) => !sections || sections.includes(id))
|
|
712
|
-
.map(({ text }) => text);
|
|
713
|
-
return {
|
|
714
|
-
id,
|
|
715
|
-
title: humanize(id),
|
|
716
|
-
content:
|
|
717
|
-
sectionFacts.length > 0
|
|
718
|
-
? sectionFacts.join(' ')
|
|
719
|
-
: 'No confirmed facts.',
|
|
720
|
-
};
|
|
721
|
-
}),
|
|
722
|
-
artifactLinks: [],
|
|
1181
|
+
footer: 'Authored from the retained reconciled fact base.',
|
|
1182
|
+
sections,
|
|
1183
|
+
artifactLinks,
|
|
723
1184
|
};
|
|
724
1185
|
}
|
|
725
1186
|
|
|
1187
|
+
function approvalArtifacts(state) {
|
|
1188
|
+
return state.resolvedArtifacts.map((artifact) => ({
|
|
1189
|
+
artifactId: artifact.id,
|
|
1190
|
+
origin: artifact.origin,
|
|
1191
|
+
...(artifact.profileId && { profileId: artifact.profileId }),
|
|
1192
|
+
authoring: artifact.authoring,
|
|
1193
|
+
contentPath:
|
|
1194
|
+
state.contentPaths.get(artifact.id) ??
|
|
1195
|
+
`source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
|
|
1196
|
+
authorResultPath:
|
|
1197
|
+
state.authorResultPaths.find((path) =>
|
|
1198
|
+
path.endsWith(`/${artifact.id}.json`),
|
|
1199
|
+
) ?? `source/author/${artifact.id}.json`,
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function expansionLinks(artifacts) {
|
|
1204
|
+
return artifacts
|
|
1205
|
+
.filter(({ origin }) => origin === 'expansion')
|
|
1206
|
+
.map((artifact) => ({
|
|
1207
|
+
id: artifact.id,
|
|
1208
|
+
type: artifact.type,
|
|
1209
|
+
label: humanize(artifact.id),
|
|
1210
|
+
origin: 'expansion',
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function expansionProfile(recipe, profileId) {
|
|
1215
|
+
return recipeExpansion(recipe).profiles.find(
|
|
1216
|
+
(profile) => profile.profileId === profileId,
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function resolvedArtifactFromApproval(recipe, persisted) {
|
|
1221
|
+
if (persisted.origin === 'floor') {
|
|
1222
|
+
const artifact = recipeFloor(recipe).find(
|
|
1223
|
+
({ id }) => id === persisted.artifactId,
|
|
1224
|
+
);
|
|
1225
|
+
if (!artifact) {
|
|
1226
|
+
throw codedError(
|
|
1227
|
+
'E_APPROVAL_RESUME',
|
|
1228
|
+
`Approval references unknown floor artifact ${persisted.artifactId}.`,
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return {
|
|
1232
|
+
...artifact,
|
|
1233
|
+
origin: 'floor',
|
|
1234
|
+
shell: artifact.authoring === 'html' ? artifact.template : undefined,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const profile = expansionProfile(recipe, persisted.profileId);
|
|
1238
|
+
if (!profile) {
|
|
1239
|
+
throw codedError(
|
|
1240
|
+
'E_APPROVAL_RESUME',
|
|
1241
|
+
`Approval references unknown expansion profile ${persisted.profileId}.`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
id: persisted.artifactId,
|
|
1246
|
+
type: profile.type,
|
|
1247
|
+
authoring: profile.authoring,
|
|
1248
|
+
briefRef: profile.briefRef,
|
|
1249
|
+
shell: profile.shell,
|
|
1250
|
+
template:
|
|
1251
|
+
profile.authoring === 'markdown'
|
|
1252
|
+
? templateForType(profile.type)
|
|
1253
|
+
: profile.shell,
|
|
1254
|
+
required: false,
|
|
1255
|
+
origin: 'expansion',
|
|
1256
|
+
profileId: profile.profileId,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function templateForType(type) {
|
|
1261
|
+
return (
|
|
1262
|
+
{
|
|
1263
|
+
hub: 'house-style',
|
|
1264
|
+
diagram: 'diagram-shell',
|
|
1265
|
+
explainer: 'engineer-tour',
|
|
1266
|
+
deck: 'deck-shell',
|
|
1267
|
+
}[type] ?? 'house-style'
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
726
1271
|
function validateRecipeSources(recipe, binding) {
|
|
727
1272
|
const primaryRole = recipe.sourceRoles[0]?.role;
|
|
728
1273
|
const bindings =
|
|
@@ -781,33 +1326,6 @@ function factBaseMarkdown(factBase) {
|
|
|
781
1326
|
return `# Fact base\n\n## Confirmed claims\n\n${confirmed || '- None.'}\n\n## Unresolved claims\n\n${unresolved || '- None.'}\n`;
|
|
782
1327
|
}
|
|
783
1328
|
|
|
784
|
-
function contentMarkdown(model) {
|
|
785
|
-
return `# ${model.title}\n\n${model.sections
|
|
786
|
-
.map(({ title, content }) => `## ${title}\n\n${content}`)
|
|
787
|
-
.join('\n\n')}\n`;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function contentModelFromMarkdown(base, markdown) {
|
|
791
|
-
const title = markdown.match(/^# (.+)$/m)?.[1]?.trim();
|
|
792
|
-
const sections = [];
|
|
793
|
-
const headings = [...markdown.matchAll(/^## (.+)$/gm)];
|
|
794
|
-
for (const [index, heading] of headings.entries()) {
|
|
795
|
-
const start = heading.index + heading[0].length;
|
|
796
|
-
const end = headings[index + 1]?.index ?? markdown.length;
|
|
797
|
-
const sectionTitle = heading[1].trim();
|
|
798
|
-
sections.push({
|
|
799
|
-
id: slugify(sectionTitle),
|
|
800
|
-
title: sectionTitle,
|
|
801
|
-
content: markdown.slice(start, end).trim(),
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
return {
|
|
805
|
-
...base,
|
|
806
|
-
...(title && { title }),
|
|
807
|
-
sections,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
1329
|
function assertValidRequest(request) {
|
|
812
1330
|
const result = validateContract('run-request', request);
|
|
813
1331
|
if (!result.valid) {
|
|
@@ -831,12 +1349,18 @@ function resultFor(state, error) {
|
|
|
831
1349
|
: state.approval?.canResume === false
|
|
832
1350
|
? 'incomplete'
|
|
833
1351
|
: 'built-not-durable',
|
|
1352
|
+
...(state.approval?.record?.marking && {
|
|
1353
|
+
marking: state.approval.record.marking,
|
|
1354
|
+
}),
|
|
834
1355
|
warnings: [...new Set(state.warnings)],
|
|
835
1356
|
discovery: state.discovery,
|
|
836
1357
|
...(state.approval && {
|
|
837
1358
|
approval: {
|
|
838
1359
|
status: state.approval.status,
|
|
839
1360
|
path: state.approval.path,
|
|
1361
|
+
...(state.approval.record.marking && {
|
|
1362
|
+
marking: state.approval.record.marking,
|
|
1363
|
+
}),
|
|
840
1364
|
},
|
|
841
1365
|
}),
|
|
842
1366
|
...(error && {
|