@open-agent-toolkit/cli 0.2.20 → 0.2.22
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 +687 -0
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +313 -14
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +314 -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 +152 -2
- package/assets/skills/explainer-kit/scripts/run.mjs +789 -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-brainstorm/SKILL.md +1 -1
- package/assets/skills/oat-brainstorm/scripts/helper.js +18 -11
- package/assets/skills/oat-brainstorm/scripts/server.cjs +109 -55
- 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/dist/commands/init/tools/index.d.ts.map +1 -1
- package/dist/commands/init/tools/index.js +4 -4
- package/dist/engine/compute-plan.js +3 -3
- 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,118 @@ 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 = approval.status === 'rejected' ? REOPENED_ON_REJECTION : [];
|
|
445
|
+
state.warnings.push(
|
|
446
|
+
...record.stages
|
|
447
|
+
.filter(({ id }) => !rerunning.includes(id))
|
|
448
|
+
.flatMap(({ warnings = [] }) =>
|
|
449
|
+
warnings.filter((warning) => !warning.startsWith('stage-reopened:')),
|
|
450
|
+
),
|
|
451
|
+
);
|
|
270
452
|
state.inputHashes = inputHashes(state.factBase);
|
|
271
453
|
state.factBaseHash = canonicalHash(state.factBase);
|
|
272
454
|
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('; ')}`,
|
|
455
|
+
const persistedArtifacts = Array.isArray(approval.artifacts)
|
|
456
|
+
? approval.artifacts
|
|
457
|
+
: recipeFloor(state.recipe).map((artifact) => ({
|
|
458
|
+
artifactId: artifact.id,
|
|
459
|
+
origin: 'floor',
|
|
460
|
+
authoring: artifact.authoring ?? 'markdown',
|
|
461
|
+
contentPath: `source/content/${artifact.id}.md`,
|
|
462
|
+
}));
|
|
463
|
+
for (const persisted of persistedArtifacts) {
|
|
464
|
+
const artifact = resolvedArtifactFromApproval(state.recipe, persisted);
|
|
465
|
+
if (artifact.authoring === 'html') {
|
|
466
|
+
artifact.shellContent = await readSkillFile(
|
|
467
|
+
`templates/${artifact.shell}.html`,
|
|
290
468
|
);
|
|
291
469
|
}
|
|
292
|
-
|
|
293
|
-
|
|
470
|
+
const content = await readFile(
|
|
471
|
+
join(state.run.runRoot, persisted.contentPath),
|
|
472
|
+
'utf8',
|
|
473
|
+
);
|
|
474
|
+
state.resolvedArtifacts.push(artifact);
|
|
475
|
+
state.authoredContent.set(artifact.id, content);
|
|
476
|
+
state.contentPaths.set(artifact.id, persisted.contentPath);
|
|
477
|
+
if (persisted.authorResultPath) {
|
|
478
|
+
state.authorResultPaths.push(persisted.authorResultPath);
|
|
479
|
+
}
|
|
294
480
|
}
|
|
481
|
+
const links = expansionLinks(state.resolvedArtifacts);
|
|
482
|
+
for (const artifact of state.resolvedArtifacts) {
|
|
483
|
+
if (artifact.authoring !== 'markdown') continue;
|
|
484
|
+
state.contentModels.push(
|
|
485
|
+
markdownContentModel(
|
|
486
|
+
artifact,
|
|
487
|
+
state.run.slug,
|
|
488
|
+
state.authoredContent.get(artifact.id),
|
|
489
|
+
artifact.origin === 'floor' ? links : [],
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
state.expansion.accepted = state.resolvedArtifacts
|
|
494
|
+
.filter(({ origin }) => origin === 'expansion')
|
|
495
|
+
.map((artifact) => ({
|
|
496
|
+
id: artifact.id,
|
|
497
|
+
profileId: artifact.profileId,
|
|
498
|
+
rationale: 'Persisted approved expansion artifact.',
|
|
499
|
+
status: 'accepted',
|
|
500
|
+
profile: expansionProfile(state.recipe, artifact.profileId),
|
|
501
|
+
}));
|
|
502
|
+
await hydrateRenderedState(state);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function hydrateRenderedState(state) {
|
|
506
|
+
state.rendered = [];
|
|
507
|
+
state.artifacts = [];
|
|
508
|
+
for (const artifact of state.resolvedArtifacts) {
|
|
509
|
+
const descriptor =
|
|
510
|
+
artifact.authoring === 'markdown'
|
|
511
|
+
? await renderArtifact({
|
|
512
|
+
recipeArtifact: renderDescriptor(artifact),
|
|
513
|
+
content: state.contentModels.find(
|
|
514
|
+
({ artifactId }) => artifactId === artifact.id,
|
|
515
|
+
),
|
|
516
|
+
theme: state.theme,
|
|
517
|
+
renderStrategy: state.renderStrategy,
|
|
518
|
+
...(state.run.request.publicBaseUrl && {
|
|
519
|
+
publicBaseUrl: state.run.request.publicBaseUrl,
|
|
520
|
+
}),
|
|
521
|
+
})
|
|
522
|
+
: artisticRender(state, artifact);
|
|
523
|
+
const html = await readFile(
|
|
524
|
+
join(state.run.runRoot, descriptor.renderedPath),
|
|
525
|
+
'utf8',
|
|
526
|
+
);
|
|
527
|
+
const rendered = { ...descriptor, html };
|
|
528
|
+
state.rendered.push(rendered);
|
|
529
|
+
state.artifacts.push(artifactRecord(state, rendered));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function artifactRecord(state, rendered) {
|
|
534
|
+
return {
|
|
535
|
+
id: rendered.artifactId,
|
|
536
|
+
type: rendered.type,
|
|
537
|
+
contentPath: state.contentPaths.get(rendered.artifactId),
|
|
538
|
+
renderedPath: rendered.renderedPath,
|
|
539
|
+
mediaType: rendered.mediaType,
|
|
540
|
+
status: 'built',
|
|
541
|
+
hash: hashBytes(rendered.html),
|
|
542
|
+
rebuildable: false,
|
|
543
|
+
};
|
|
295
544
|
}
|
|
296
545
|
|
|
297
546
|
async function readJson(path) {
|
|
@@ -338,7 +587,7 @@ async function executeStage(run, id, options, operation) {
|
|
|
338
587
|
id,
|
|
339
588
|
status: result.status ?? 'passed',
|
|
340
589
|
outputPaths: result.outputPaths ?? [],
|
|
341
|
-
warnings: result.warnings
|
|
590
|
+
...(result.warnings !== undefined && { warnings: result.warnings }),
|
|
342
591
|
});
|
|
343
592
|
} catch (error) {
|
|
344
593
|
await updateBuildRecord(run, {
|
|
@@ -512,7 +761,7 @@ async function persistFailureManifest(state, error, createdAt) {
|
|
|
512
761
|
const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
|
|
513
762
|
const recordedIds = new Set(state.artifacts.map(({ id }) => id));
|
|
514
763
|
state.artifacts.push(
|
|
515
|
-
...state.recipe
|
|
764
|
+
...recipeFloor(state.recipe)
|
|
516
765
|
.filter(({ id }) => !recordedIds.has(id))
|
|
517
766
|
.map((artifact) => ({
|
|
518
767
|
id: artifact.id,
|
|
@@ -567,162 +816,451 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
|
|
|
567
816
|
};
|
|
568
817
|
}
|
|
569
818
|
|
|
570
|
-
async function createAuthoredContent(state,
|
|
819
|
+
async function createAuthoredContent(state, options, now) {
|
|
820
|
+
const author = options.author;
|
|
571
821
|
if (typeof author !== 'function') {
|
|
572
822
|
throw codedError(
|
|
573
823
|
'E_AUTHOR_REQUIRED',
|
|
574
|
-
'
|
|
824
|
+
'Explainer runs require an explicit author callback in both modes.',
|
|
575
825
|
);
|
|
576
826
|
}
|
|
827
|
+
const trust = authorTrustContext(options, now);
|
|
577
828
|
|
|
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
|
-
}
|
|
829
|
+
const floor = await Promise.all(
|
|
830
|
+
recipeFloor(state.recipe).map((artifact) =>
|
|
831
|
+
authorArtifact(
|
|
832
|
+
state,
|
|
833
|
+
{
|
|
834
|
+
...artifact,
|
|
835
|
+
origin: 'floor',
|
|
836
|
+
shell: artifact.authoring === 'html' ? artifact.template : undefined,
|
|
837
|
+
},
|
|
838
|
+
author,
|
|
839
|
+
trust,
|
|
840
|
+
),
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
const proposals = floor.flatMap(
|
|
844
|
+
({ result }) => result.proposedArtifacts ?? [],
|
|
845
|
+
);
|
|
846
|
+
state.expansion = evaluateExpansionProposals(state.recipe, proposals);
|
|
847
|
+
if (!state.expansion.valid) {
|
|
848
|
+
throw codedError(
|
|
849
|
+
'E_AUTHOR_RESULT',
|
|
850
|
+
`Invalid expansion proposals: ${state.expansion.errors.join('; ')}`,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
607
853
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
854
|
+
const expansions = [];
|
|
855
|
+
for (const accepted of state.expansion.accepted) {
|
|
856
|
+
const profile = accepted.profile;
|
|
857
|
+
const item = await authorArtifact(
|
|
858
|
+
state,
|
|
859
|
+
{
|
|
860
|
+
id: accepted.id,
|
|
861
|
+
type: profile.type,
|
|
862
|
+
authoring: profile.authoring,
|
|
863
|
+
briefRef: profile.briefRef,
|
|
864
|
+
shell: profile.shell,
|
|
865
|
+
template:
|
|
866
|
+
profile.authoring === 'markdown'
|
|
867
|
+
? templateForType(profile.type)
|
|
868
|
+
: profile.shell,
|
|
869
|
+
required: false,
|
|
870
|
+
origin: 'expansion',
|
|
871
|
+
profileId: profile.profileId,
|
|
872
|
+
},
|
|
873
|
+
author,
|
|
874
|
+
trust,
|
|
875
|
+
);
|
|
876
|
+
if ((item.result.proposedArtifacts ?? []).length > 0) {
|
|
624
877
|
throw codedError(
|
|
625
878
|
'E_AUTHOR_RESULT',
|
|
626
|
-
`
|
|
879
|
+
`Expansion artifact ${accepted.id} cannot propose nested artifacts.`,
|
|
627
880
|
);
|
|
628
881
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
882
|
+
expansions.push(item);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const authored = [...floor, ...expansions];
|
|
886
|
+
state.resolvedArtifacts = authored.map(({ artifact }) => artifact);
|
|
887
|
+
const links = expansionLinks(state.resolvedArtifacts);
|
|
888
|
+
for (const item of authored) {
|
|
889
|
+
await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
|
|
890
|
+
await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
|
|
891
|
+
state.authorResultPaths.push(item.resultPath);
|
|
892
|
+
state.contentPaths.set(item.artifact.id, item.contentPath);
|
|
893
|
+
state.authoredContent.set(item.artifact.id, item.content);
|
|
894
|
+
if (item.artifact.authoring === 'markdown') {
|
|
895
|
+
state.contentModels.push(
|
|
896
|
+
assertValidContentModel(
|
|
897
|
+
state.recipe,
|
|
898
|
+
markdownContentModel(
|
|
899
|
+
item.artifact,
|
|
900
|
+
state.run.slug,
|
|
901
|
+
item.content,
|
|
902
|
+
item.artifact.origin === 'floor' ? links : [],
|
|
903
|
+
),
|
|
904
|
+
item.artifact,
|
|
905
|
+
),
|
|
643
906
|
);
|
|
644
907
|
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
645
910
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
sections: result.content.sections.map(({ id, title, prose }) => ({
|
|
657
|
-
id,
|
|
658
|
-
title,
|
|
659
|
-
content: prose,
|
|
660
|
-
})),
|
|
661
|
-
artifactLinks: result.content.artifactLinks ?? [],
|
|
662
|
-
},
|
|
663
|
-
});
|
|
911
|
+
// Expansion artifacts are not recipe-floor entries, so only floor content models
|
|
912
|
+
// can be checked against the floor-scoped contract.
|
|
913
|
+
function assertValidContentModel(recipe, model, artifact) {
|
|
914
|
+
if (artifact.origin !== 'floor') return model;
|
|
915
|
+
const result = validateContentModel(recipe, model);
|
|
916
|
+
if (!result.valid) {
|
|
917
|
+
throw codedError(
|
|
918
|
+
'E_CONTENT_MODEL',
|
|
919
|
+
`Authored content for ${artifact.id} violates the narrative contract: ${result.errors.join('; ')}`,
|
|
920
|
+
);
|
|
664
921
|
}
|
|
922
|
+
return model;
|
|
923
|
+
}
|
|
665
924
|
|
|
666
|
-
|
|
667
|
-
|
|
925
|
+
// Provenance authenticity cannot come from the party being identified, so
|
|
926
|
+
// identity and method are bound to trusted caller configuration and the
|
|
927
|
+
// generation time is stamped from the run's injected clock.
|
|
928
|
+
function authorTrustContext(options, now) {
|
|
929
|
+
const generatedAt = now();
|
|
930
|
+
const declared = options.authorProvenance;
|
|
931
|
+
if (declared === undefined) return { generatedAt, bound: null };
|
|
932
|
+
const valid =
|
|
933
|
+
typeof declared === 'object' &&
|
|
934
|
+
declared !== null &&
|
|
935
|
+
!Array.isArray(declared) &&
|
|
936
|
+
typeof declared.authorId === 'string' &&
|
|
937
|
+
declared.authorId.length > 0 &&
|
|
938
|
+
(declared.method === undefined ||
|
|
939
|
+
(typeof declared.method === 'string' && declared.method.length > 0));
|
|
940
|
+
if (!valid) {
|
|
941
|
+
throw codedError(
|
|
942
|
+
'E_AUTHOR_PROVENANCE',
|
|
943
|
+
'Trusted author provenance requires a non-empty authorId and, when present, a non-empty method.',
|
|
944
|
+
);
|
|
668
945
|
}
|
|
669
946
|
return {
|
|
670
|
-
|
|
671
|
-
|
|
947
|
+
generatedAt,
|
|
948
|
+
bound: {
|
|
949
|
+
authorId: declared.authorId,
|
|
950
|
+
...(declared.method !== undefined && { method: declared.method }),
|
|
951
|
+
},
|
|
672
952
|
};
|
|
673
953
|
}
|
|
674
954
|
|
|
955
|
+
function resolveAuthorProvenance(claimed, trust, artifactId) {
|
|
956
|
+
if ('trust' in claimed) {
|
|
957
|
+
throw codedError(
|
|
958
|
+
'E_AUTHOR_PROVENANCE',
|
|
959
|
+
`Author result for ${artifactId} must not assert a provenance trust level; the core stamps it.`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
if (trust.bound === null) {
|
|
963
|
+
return {
|
|
964
|
+
...claimed,
|
|
965
|
+
generatedAt: trust.generatedAt,
|
|
966
|
+
trust: 'self-asserted',
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (
|
|
970
|
+
claimed.authorId !== trust.bound.authorId ||
|
|
971
|
+
(trust.bound.method !== undefined && claimed.method !== trust.bound.method)
|
|
972
|
+
) {
|
|
973
|
+
throw codedError(
|
|
974
|
+
'E_AUTHOR_PROVENANCE',
|
|
975
|
+
`Author result for ${artifactId} claims provenance that does not match the trusted caller context.`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
...trust.bound,
|
|
980
|
+
generatedAt: trust.generatedAt,
|
|
981
|
+
trust: 'caller-bound',
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function authorArtifact(state, artifact, author, trust) {
|
|
986
|
+
const brief = await readSkillFile(artifact.briefRef);
|
|
987
|
+
const shellContent =
|
|
988
|
+
artifact.authoring === 'html'
|
|
989
|
+
? await readSkillFile(`templates/${artifact.shell}.html`)
|
|
990
|
+
: undefined;
|
|
991
|
+
const resolvedArtifact = {
|
|
992
|
+
...artifact,
|
|
993
|
+
...(shellContent && { shellContent }),
|
|
994
|
+
};
|
|
995
|
+
const requiredNarrative =
|
|
996
|
+
artifact.origin === 'floor'
|
|
997
|
+
? recipeRequiredNarrative(state.recipe, artifact.id)
|
|
998
|
+
: [];
|
|
999
|
+
const authorRequest = {
|
|
1000
|
+
schemaVersion: 'explainer-kit.author-request/v2',
|
|
1001
|
+
artifactId: artifact.id,
|
|
1002
|
+
artifactType: artifact.type,
|
|
1003
|
+
authoring: artifact.authoring,
|
|
1004
|
+
brief,
|
|
1005
|
+
factBase: structuredClone(state.factBase),
|
|
1006
|
+
...(shellContent && { shell: shellContent }),
|
|
1007
|
+
theme: structuredClone(state.theme),
|
|
1008
|
+
...(artifact.origin === 'floor' &&
|
|
1009
|
+
requiredNarrative.length > 0 && {
|
|
1010
|
+
floor: { requiredNarrative },
|
|
1011
|
+
}),
|
|
1012
|
+
};
|
|
1013
|
+
const requestValidation = validateContract(
|
|
1014
|
+
'author-request/v2',
|
|
1015
|
+
authorRequest,
|
|
1016
|
+
);
|
|
1017
|
+
if (!requestValidation.valid) {
|
|
1018
|
+
throw codedError(
|
|
1019
|
+
'E_AUTHOR_REQUEST',
|
|
1020
|
+
contractErrorMessage('author request', requestValidation.errors),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const result = await author(structuredClone(authorRequest));
|
|
1025
|
+
const resultValidation = validateContract('author-result/v2', result);
|
|
1026
|
+
if (!resultValidation.valid) {
|
|
1027
|
+
throw codedError(
|
|
1028
|
+
'E_AUTHOR_RESULT',
|
|
1029
|
+
contractErrorMessage('author result', resultValidation.errors),
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const content = result.content?.[artifact.authoring];
|
|
1033
|
+
if (result.artifactId !== artifact.id || typeof content !== 'string') {
|
|
1034
|
+
throw codedError(
|
|
1035
|
+
'E_AUTHOR_RESULT',
|
|
1036
|
+
`Author result for ${artifact.id} must match its identity and ${artifact.authoring} path.`,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const retained = {
|
|
1040
|
+
...structuredClone(result),
|
|
1041
|
+
provenance: resolveAuthorProvenance(result.provenance, trust, artifact.id),
|
|
1042
|
+
};
|
|
1043
|
+
const retainedValidation = validateContract('author-result/v2', retained);
|
|
1044
|
+
if (!retainedValidation.valid) {
|
|
1045
|
+
throw codedError(
|
|
1046
|
+
'E_AUTHOR_PROVENANCE',
|
|
1047
|
+
contractErrorMessage('retained author result', retainedValidation.errors),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
const dumpCheck = checkSourceDumping({
|
|
1051
|
+
authoredText: content,
|
|
1052
|
+
sourceTexts: [
|
|
1053
|
+
...state.factBase.claims,
|
|
1054
|
+
...state.factBase.unresolvedClaims,
|
|
1055
|
+
].map(({ text }) => text),
|
|
1056
|
+
});
|
|
1057
|
+
state.qaErrors.push(
|
|
1058
|
+
...dumpCheck.issues.map((issue) => ({
|
|
1059
|
+
code: issue.code,
|
|
1060
|
+
message: issue.message,
|
|
1061
|
+
})),
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
artifact: resolvedArtifact,
|
|
1066
|
+
result: retained,
|
|
1067
|
+
resultPath: `source/author/${artifact.id}.json`,
|
|
1068
|
+
content,
|
|
1069
|
+
contentPath: `source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function readSkillFile(relativePath) {
|
|
1074
|
+
return readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8');
|
|
1075
|
+
}
|
|
1076
|
+
|
|
675
1077
|
function contractErrorMessage(label, errors) {
|
|
676
1078
|
return `Invalid ${label}: ${errors
|
|
677
1079
|
.map(({ path, message }) => `${path}: ${message}`)
|
|
678
1080
|
.join('; ')}`;
|
|
679
1081
|
}
|
|
680
1082
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1083
|
+
// renderArtifact validates an exact key set, so normalized v2 floor entries
|
|
1084
|
+
// must be narrowed before they reach it.
|
|
1085
|
+
function renderDescriptor(artifact) {
|
|
1086
|
+
return {
|
|
1087
|
+
id: artifact.id,
|
|
1088
|
+
type: artifact.type,
|
|
1089
|
+
template: artifact.template,
|
|
1090
|
+
required: artifact.required,
|
|
1091
|
+
origin: artifact.origin,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const LEAD_SECTION_IDS = ['overview', 'introduction', 'lead'];
|
|
1096
|
+
|
|
1097
|
+
function markdownContentModel(artifact, slug, markdown, artifactLinks) {
|
|
1098
|
+
const titleMatch = markdown.match(/^# (.+)$/m);
|
|
1099
|
+
const title = titleMatch?.[1]?.trim() ?? humanize(artifact.id);
|
|
1100
|
+
const headings = [...markdown.matchAll(/^## (.+)$/gm)];
|
|
1101
|
+
if (headings.length === 0) {
|
|
1102
|
+
return contentModel({
|
|
1103
|
+
artifact,
|
|
1104
|
+
slug,
|
|
1105
|
+
title,
|
|
1106
|
+
artifactLinks,
|
|
1107
|
+
sections: [{ id: 'overview', title: 'Overview', content: markdown }],
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Prose between the document title and the first `##` is authored content,
|
|
1112
|
+
// so it is carried as a leading section rather than silently dropped.
|
|
1113
|
+
const bodyStart = titleMatch ? titleMatch.index + titleMatch[0].length : 0;
|
|
1114
|
+
const lead = markdown.slice(bodyStart, headings[0].index).trim();
|
|
1115
|
+
const authoredIds = new Set(
|
|
1116
|
+
headings.map((heading) => slugify(heading[1].trim())),
|
|
1117
|
+
);
|
|
1118
|
+
const sections = headings.map((heading, index) => ({
|
|
1119
|
+
id: slugify(heading[1].trim()),
|
|
1120
|
+
title: heading[1].trim(),
|
|
1121
|
+
content: markdown
|
|
1122
|
+
.slice(
|
|
1123
|
+
heading.index + heading[0].length,
|
|
1124
|
+
headings[index + 1]?.index ?? markdown.length,
|
|
1125
|
+
)
|
|
1126
|
+
.trim(),
|
|
1127
|
+
}));
|
|
1128
|
+
if (lead.length > 0) {
|
|
1129
|
+
const leadId =
|
|
1130
|
+
LEAD_SECTION_IDS.find((candidate) => !authoredIds.has(candidate)) ??
|
|
1131
|
+
'lead';
|
|
1132
|
+
sections.unshift({
|
|
1133
|
+
id: leadId,
|
|
1134
|
+
title: humanize(leadId),
|
|
1135
|
+
content: lead,
|
|
1136
|
+
});
|
|
701
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
return contentModel({
|
|
1140
|
+
artifact,
|
|
1141
|
+
slug,
|
|
1142
|
+
title,
|
|
1143
|
+
artifactLinks,
|
|
1144
|
+
sections: disambiguateSectionIds(sections),
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// A repeated heading is legitimate authoring, but duplicate anchors break
|
|
1149
|
+
// navigation, so later collisions get a deterministic suffix.
|
|
1150
|
+
function disambiguateSectionIds(sections) {
|
|
1151
|
+
const used = new Map();
|
|
1152
|
+
return sections.map((section) => {
|
|
1153
|
+
const seen = used.get(section.id) ?? 0;
|
|
1154
|
+
used.set(section.id, seen + 1);
|
|
1155
|
+
if (seen === 0) return section;
|
|
1156
|
+
let candidate = `${section.id}-${seen + 1}`;
|
|
1157
|
+
let offset = seen + 1;
|
|
1158
|
+
while (used.has(candidate)) {
|
|
1159
|
+
offset += 1;
|
|
1160
|
+
candidate = `${section.id}-${offset}`;
|
|
1161
|
+
}
|
|
1162
|
+
used.set(candidate, 1);
|
|
1163
|
+
return { ...section, id: candidate };
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function contentModel({ artifact, slug, title, artifactLinks, sections }) {
|
|
702
1168
|
return {
|
|
703
1169
|
artifactId: artifact.id,
|
|
704
1170
|
slug,
|
|
705
|
-
title
|
|
706
|
-
description: `
|
|
1171
|
+
title,
|
|
1172
|
+
description: `Authored ${humanize(artifact.id).toLowerCase()}.`,
|
|
707
1173
|
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: [],
|
|
1174
|
+
footer: 'Authored from the retained reconciled fact base.',
|
|
1175
|
+
sections,
|
|
1176
|
+
artifactLinks,
|
|
723
1177
|
};
|
|
724
1178
|
}
|
|
725
1179
|
|
|
1180
|
+
function approvalArtifacts(state) {
|
|
1181
|
+
return state.resolvedArtifacts.map((artifact) => ({
|
|
1182
|
+
artifactId: artifact.id,
|
|
1183
|
+
origin: artifact.origin,
|
|
1184
|
+
...(artifact.profileId && { profileId: artifact.profileId }),
|
|
1185
|
+
authoring: artifact.authoring,
|
|
1186
|
+
contentPath:
|
|
1187
|
+
state.contentPaths.get(artifact.id) ??
|
|
1188
|
+
`source/content/${artifact.id}.${artifact.authoring === 'markdown' ? 'md' : 'html'}`,
|
|
1189
|
+
authorResultPath:
|
|
1190
|
+
state.authorResultPaths.find((path) =>
|
|
1191
|
+
path.endsWith(`/${artifact.id}.json`),
|
|
1192
|
+
) ?? `source/author/${artifact.id}.json`,
|
|
1193
|
+
}));
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function expansionLinks(artifacts) {
|
|
1197
|
+
return artifacts
|
|
1198
|
+
.filter(({ origin }) => origin === 'expansion')
|
|
1199
|
+
.map((artifact) => ({
|
|
1200
|
+
id: artifact.id,
|
|
1201
|
+
type: artifact.type,
|
|
1202
|
+
label: humanize(artifact.id),
|
|
1203
|
+
origin: 'expansion',
|
|
1204
|
+
}));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function expansionProfile(recipe, profileId) {
|
|
1208
|
+
return recipeExpansion(recipe).profiles.find(
|
|
1209
|
+
(profile) => profile.profileId === profileId,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function resolvedArtifactFromApproval(recipe, persisted) {
|
|
1214
|
+
if (persisted.origin === 'floor') {
|
|
1215
|
+
const artifact = recipeFloor(recipe).find(
|
|
1216
|
+
({ id }) => id === persisted.artifactId,
|
|
1217
|
+
);
|
|
1218
|
+
if (!artifact) {
|
|
1219
|
+
throw codedError(
|
|
1220
|
+
'E_APPROVAL_RESUME',
|
|
1221
|
+
`Approval references unknown floor artifact ${persisted.artifactId}.`,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
...artifact,
|
|
1226
|
+
origin: 'floor',
|
|
1227
|
+
shell: artifact.authoring === 'html' ? artifact.template : undefined,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const profile = expansionProfile(recipe, persisted.profileId);
|
|
1231
|
+
if (!profile) {
|
|
1232
|
+
throw codedError(
|
|
1233
|
+
'E_APPROVAL_RESUME',
|
|
1234
|
+
`Approval references unknown expansion profile ${persisted.profileId}.`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
id: persisted.artifactId,
|
|
1239
|
+
type: profile.type,
|
|
1240
|
+
authoring: profile.authoring,
|
|
1241
|
+
briefRef: profile.briefRef,
|
|
1242
|
+
shell: profile.shell,
|
|
1243
|
+
template:
|
|
1244
|
+
profile.authoring === 'markdown'
|
|
1245
|
+
? templateForType(profile.type)
|
|
1246
|
+
: profile.shell,
|
|
1247
|
+
required: false,
|
|
1248
|
+
origin: 'expansion',
|
|
1249
|
+
profileId: profile.profileId,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function templateForType(type) {
|
|
1254
|
+
return (
|
|
1255
|
+
{
|
|
1256
|
+
hub: 'house-style',
|
|
1257
|
+
diagram: 'diagram-shell',
|
|
1258
|
+
explainer: 'engineer-tour',
|
|
1259
|
+
deck: 'deck-shell',
|
|
1260
|
+
}[type] ?? 'house-style'
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
726
1264
|
function validateRecipeSources(recipe, binding) {
|
|
727
1265
|
const primaryRole = recipe.sourceRoles[0]?.role;
|
|
728
1266
|
const bindings =
|
|
@@ -781,33 +1319,6 @@ function factBaseMarkdown(factBase) {
|
|
|
781
1319
|
return `# Fact base\n\n## Confirmed claims\n\n${confirmed || '- None.'}\n\n## Unresolved claims\n\n${unresolved || '- None.'}\n`;
|
|
782
1320
|
}
|
|
783
1321
|
|
|
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
1322
|
function assertValidRequest(request) {
|
|
812
1323
|
const result = validateContract('run-request', request);
|
|
813
1324
|
if (!result.valid) {
|
|
@@ -831,12 +1342,18 @@ function resultFor(state, error) {
|
|
|
831
1342
|
: state.approval?.canResume === false
|
|
832
1343
|
? 'incomplete'
|
|
833
1344
|
: 'built-not-durable',
|
|
1345
|
+
...(state.approval?.record?.marking && {
|
|
1346
|
+
marking: state.approval.record.marking,
|
|
1347
|
+
}),
|
|
834
1348
|
warnings: [...new Set(state.warnings)],
|
|
835
1349
|
discovery: state.discovery,
|
|
836
1350
|
...(state.approval && {
|
|
837
1351
|
approval: {
|
|
838
1352
|
status: state.approval.status,
|
|
839
1353
|
path: state.approval.path,
|
|
1354
|
+
...(state.approval.record.marking && {
|
|
1355
|
+
marking: state.approval.record.marking,
|
|
1356
|
+
}),
|
|
840
1357
|
},
|
|
841
1358
|
}),
|
|
842
1359
|
...(error && {
|