@open-agent-toolkit/cli 0.2.30 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +41 -0
- package/assets/docs/contributing/code.md +19 -7
- package/assets/docs/contributing/explainer-kit-verification.md +9 -1
- package/assets/docs/contributing/skills.md +9 -0
- package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
- package/assets/docs/workflows/skills/explainer-kit.md +98 -38
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +33 -8
- package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
- package/assets/skills/explainer-kit/references/contracts.md +49 -17
- package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
- package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
- package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
- package/assets/skills/explainer-kit/references/visual-review.md +19 -5
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
- package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
- package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
- package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
- package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
- package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +19 -3
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +18 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +57 -7
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
- package/dist/config/oat-config.d.ts +2 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +4 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +1 -0
- package/package.json +2 -2
- /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
5
|
-
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
|
|
8
8
|
import { assertBrowserProbeSession } from './lib/browser-runtime.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
catalogFromManifest,
|
|
11
|
+
initiativeCatalogPath,
|
|
12
|
+
serializeInitiativeCatalog,
|
|
13
|
+
} from './lib/catalog.mjs';
|
|
10
14
|
import {
|
|
11
15
|
readContentApproval,
|
|
12
16
|
resolveContentApproval,
|
|
@@ -20,7 +24,10 @@ import {
|
|
|
20
24
|
import { processFactBase } from './lib/fact-base.mjs';
|
|
21
25
|
import { writeJsonAtomic, writeTextAtomic } from './lib/fs-safe.mjs';
|
|
22
26
|
import { validateHtmlSafety } from './lib/html-safety.mjs';
|
|
27
|
+
import { validateInternalReferences } from './lib/internal-references.mjs';
|
|
23
28
|
import { parseMarkdown } from './lib/markdown.mjs';
|
|
29
|
+
import { enforceRunPackageInventory } from './lib/package-coverage.mjs';
|
|
30
|
+
import { assertManifestPublishable } from './lib/publication-policy.mjs';
|
|
24
31
|
import {
|
|
25
32
|
auditArtifactSet,
|
|
26
33
|
checkGuidelines,
|
|
@@ -47,10 +54,12 @@ import {
|
|
|
47
54
|
initializeRun,
|
|
48
55
|
readSetPlanRecords,
|
|
49
56
|
reopenBuildStages,
|
|
57
|
+
supersedeTerminalEvidence,
|
|
50
58
|
updateBuildRecord,
|
|
51
59
|
verifySetPlanResumeToken,
|
|
52
60
|
writeManifestAtomic,
|
|
53
61
|
writeSetPlanRecords,
|
|
62
|
+
writeTerminalEvidence,
|
|
54
63
|
writeVisualReviewAttempt,
|
|
55
64
|
writeVisualReviewFailure,
|
|
56
65
|
writeVisualRevision,
|
|
@@ -58,6 +67,11 @@ import {
|
|
|
58
67
|
import { artifactPath, renderArtifact } from './lib/render.mjs';
|
|
59
68
|
import { resolveRootConfinedPath } from './lib/safe-paths.mjs';
|
|
60
69
|
import { plannedArtifacts, planExplainerSet } from './lib/set-plan.mjs';
|
|
70
|
+
import {
|
|
71
|
+
createVisualReviewEvidence,
|
|
72
|
+
evidenceReason,
|
|
73
|
+
projectThrownReason,
|
|
74
|
+
} from './lib/terminal-evidence.mjs';
|
|
61
75
|
import { resolveTheme } from './lib/theme.mjs';
|
|
62
76
|
import { runVisualReview } from './lib/visual-review.mjs';
|
|
63
77
|
|
|
@@ -69,6 +83,7 @@ export {
|
|
|
69
83
|
|
|
70
84
|
// Stages a rejected draft reruns once its content is corrected.
|
|
71
85
|
const REOPENED_ON_REJECTION = Object.freeze(['render', 'qa']);
|
|
86
|
+
const LOCALLY_PROJECTED_ERRORS = new WeakSet();
|
|
72
87
|
|
|
73
88
|
export async function runExplainer(request, options = {}) {
|
|
74
89
|
const normalizedRequest = normalizeRunRequest(request);
|
|
@@ -124,6 +139,9 @@ export async function runExplainer(request, options = {}) {
|
|
|
124
139
|
visualReviewPaths: [],
|
|
125
140
|
visualReviewAttempt: 0,
|
|
126
141
|
reviewGateBlocked: false,
|
|
142
|
+
reviewGateReason: null,
|
|
143
|
+
correctionAttempted: false,
|
|
144
|
+
publishReceiptPath: null,
|
|
127
145
|
resumeToken: null,
|
|
128
146
|
resumedApprovalStatus: null,
|
|
129
147
|
};
|
|
@@ -143,7 +161,11 @@ export async function runExplainer(request, options = {}) {
|
|
|
143
161
|
now,
|
|
144
162
|
);
|
|
145
163
|
state.factBase = processed.factBase;
|
|
146
|
-
|
|
164
|
+
const factBaseWarnings =
|
|
165
|
+
processed.checks.warnings.length > 0
|
|
166
|
+
? ['fact-base-freshness-warning']
|
|
167
|
+
: [];
|
|
168
|
+
state.warnings.push(...factBaseWarnings);
|
|
147
169
|
state.inputHashes = inputHashes(processed.factBase);
|
|
148
170
|
state.factBaseHash = canonicalHash(processed.factBase);
|
|
149
171
|
await writeJsonAtomic(
|
|
@@ -158,8 +180,8 @@ export async function runExplainer(request, options = {}) {
|
|
|
158
180
|
);
|
|
159
181
|
return {
|
|
160
182
|
outputPaths: ['source/fact-base.json', 'source/fact-base.md'],
|
|
161
|
-
warnings:
|
|
162
|
-
status:
|
|
183
|
+
warnings: factBaseWarnings,
|
|
184
|
+
status: factBaseWarnings.length > 0 ? 'warned' : 'passed',
|
|
163
185
|
};
|
|
164
186
|
});
|
|
165
187
|
await prepareTheme(state);
|
|
@@ -229,13 +251,27 @@ export async function runExplainer(request, options = {}) {
|
|
|
229
251
|
if (run.request.mode === 'interactive') {
|
|
230
252
|
state.resumeToken = await createSetPlanResumeToken(run);
|
|
231
253
|
}
|
|
254
|
+
const manifest = await inventoryManifestFor(state, now());
|
|
255
|
+
await enforceRetainedRunPackage(state, manifest, {
|
|
256
|
+
includeManifest: false,
|
|
257
|
+
failureStage: 'qa',
|
|
258
|
+
});
|
|
232
259
|
return resultFor(state);
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
if (state.reviewGateBlocked) {
|
|
236
263
|
await updateBuildRecord(run, { id: 'durability', status: 'skipped' });
|
|
237
264
|
await updateBuildRecord(run, { id: 'publish', status: 'skipped' });
|
|
238
|
-
await persistManifest(state, now());
|
|
265
|
+
const manifest = await persistManifest(state, now());
|
|
266
|
+
await enforceRetainedRunPackage(state, manifest, {
|
|
267
|
+
acceptCleanedUnexpected: true,
|
|
268
|
+
});
|
|
269
|
+
await writeTerminalEvidence(run, {
|
|
270
|
+
outcome: 'built-needs-review',
|
|
271
|
+
manifest,
|
|
272
|
+
reasons: terminalReasonsForReview(state),
|
|
273
|
+
evidenceDisposition: state.visualReview ? 'retained' : 'partial',
|
|
274
|
+
});
|
|
239
275
|
return resultFor(state);
|
|
240
276
|
}
|
|
241
277
|
|
|
@@ -245,17 +281,119 @@ export async function runExplainer(request, options = {}) {
|
|
|
245
281
|
now,
|
|
246
282
|
);
|
|
247
283
|
if (!manifestFinalized) {
|
|
248
|
-
await persistManifest(state, now());
|
|
284
|
+
const manifest = await persistManifest(state, now());
|
|
285
|
+
await enforceRetainedRunPackage(state, manifest, {
|
|
286
|
+
failureStage: 'qa',
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
const manifest = JSON.parse(
|
|
290
|
+
await readFile(state.run.manifestPath, 'utf8'),
|
|
291
|
+
);
|
|
292
|
+
await enforceRetainedRunPackage(state, manifest, {
|
|
293
|
+
failureStage: 'qa',
|
|
294
|
+
});
|
|
249
295
|
}
|
|
250
296
|
return resultFor(state);
|
|
251
297
|
} catch (error) {
|
|
298
|
+
let manifest;
|
|
252
299
|
if (state.theme && state.factBase) {
|
|
253
300
|
await persistFailureManifest(state, error, now()).catch(() => {});
|
|
301
|
+
manifest = await readFile(state.run.manifestPath, 'utf8')
|
|
302
|
+
.then(JSON.parse)
|
|
303
|
+
.catch(() => undefined);
|
|
304
|
+
}
|
|
305
|
+
const reason =
|
|
306
|
+
localEvidenceReason(error) ??
|
|
307
|
+
projectThrownReason('finalization', 'pipeline-failure');
|
|
308
|
+
// The inventory verdict is reached *before* terminal evidence is written,
|
|
309
|
+
// so the single write below carries the complete reason set. Terminal
|
|
310
|
+
// evidence is immutable once retained (`records.mjs` refuses a second
|
|
311
|
+
// write unless the path is absent), so a re-record after the fact could
|
|
312
|
+
// never succeed; the previous shape swallowed that guaranteed failure in a
|
|
313
|
+
// `.catch(() => {})` under a comment claiming the durable evidence was
|
|
314
|
+
// updated. `includeTerminalEvidence` is correspondingly dropped here,
|
|
315
|
+
// because the file legitimately does not exist yet at this point.
|
|
316
|
+
let reasons = [reason];
|
|
317
|
+
if (manifest) {
|
|
318
|
+
try {
|
|
319
|
+
await enforceRetainedRunPackage(state, manifest);
|
|
320
|
+
} catch (inventoryError) {
|
|
321
|
+
// This used to be swallowed outright. It must not simply be reported
|
|
322
|
+
// either: a failed run routinely leaves partial outputs, and the first
|
|
323
|
+
// pass removes them and *then* throws, so the common case is a
|
|
324
|
+
// successful repair rather than a violated invariant. Re-running
|
|
325
|
+
// separates the two — if the tree is clean now, the repair worked and
|
|
326
|
+
// there is nothing to report; if it still fails, the violation is
|
|
327
|
+
// genuinely unremovable or a required file is missing, which is the
|
|
328
|
+
// case that previously went unreported at run time.
|
|
329
|
+
const repaired = await enforceRetainedRunPackage(state, manifest).then(
|
|
330
|
+
() => true,
|
|
331
|
+
() => false,
|
|
332
|
+
);
|
|
333
|
+
if (!repaired) {
|
|
334
|
+
reasons = mergeTerminalReasons(
|
|
335
|
+
reasons,
|
|
336
|
+
localEvidenceReason(inventoryError) ??
|
|
337
|
+
projectThrownReason('finalization', 'pipeline-failure'),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
254
341
|
}
|
|
255
|
-
|
|
342
|
+
await writeTerminalEvidence(run, {
|
|
343
|
+
outcome: 'failed',
|
|
344
|
+
manifest,
|
|
345
|
+
reasons,
|
|
346
|
+
evidenceDisposition: manifest ? 'retained' : 'unavailable',
|
|
347
|
+
});
|
|
348
|
+
return resultFor(state, { failed: true, reasons });
|
|
256
349
|
}
|
|
257
350
|
}
|
|
258
351
|
|
|
352
|
+
export async function supersedeExplainerRun({ runRoot, supersededBy } = {}) {
|
|
353
|
+
if (typeof runRoot !== 'string') {
|
|
354
|
+
throw new TypeError('Supersession requires a runRoot.');
|
|
355
|
+
}
|
|
356
|
+
const confinedRunRoot = await realpath(runRoot);
|
|
357
|
+
const manifest = JSON.parse(
|
|
358
|
+
await readFile(join(confinedRunRoot, 'manifest.json'), 'utf8'),
|
|
359
|
+
);
|
|
360
|
+
if (
|
|
361
|
+
manifest?.schemaVersion !== 'explainer-kit.manifest/v1' ||
|
|
362
|
+
typeof manifest.runId !== 'string' ||
|
|
363
|
+
typeof manifest.slug !== 'string' ||
|
|
364
|
+
!['built-needs-review', 'failed'].includes(manifest.outcome)
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
'Only a flagged or failed manifest can produce supersession evidence.',
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (
|
|
371
|
+
!supersededBy ||
|
|
372
|
+
typeof supersededBy !== 'object' ||
|
|
373
|
+
typeof supersededBy.runId !== 'string' ||
|
|
374
|
+
supersededBy.runId === manifest.runId ||
|
|
375
|
+
!/^sha256:[a-f0-9]{64}$/.test(supersededBy.manifestHash ?? '')
|
|
376
|
+
) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
'Supersession requires a distinct replacement run ID and manifest hash.',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
await supersedeTerminalEvidence(
|
|
382
|
+
{
|
|
383
|
+
runId: manifest.runId,
|
|
384
|
+
slug: manifest.slug,
|
|
385
|
+
runRoot: confinedRunRoot,
|
|
386
|
+
},
|
|
387
|
+
{ manifest, supersededBy },
|
|
388
|
+
);
|
|
389
|
+
return {
|
|
390
|
+
runId: manifest.runId,
|
|
391
|
+
outcome: manifest.outcome,
|
|
392
|
+
terminalEvidencePath: join(confinedRunRoot, 'terminal-evidence.json'),
|
|
393
|
+
supersededBy: structuredClone(supersededBy),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
259
397
|
async function executeThemeStage(state, options) {
|
|
260
398
|
await executeStage(state.run, 'theme', options, async () => {
|
|
261
399
|
await writeJsonAtomic(
|
|
@@ -275,8 +413,9 @@ async function prepareTheme(state) {
|
|
|
275
413
|
const resolved = await resolveTheme(state.run.request.theme);
|
|
276
414
|
state.theme = resolved.theme;
|
|
277
415
|
state.renderStrategy = resolved.renderStrategy;
|
|
278
|
-
state.themeWarnings =
|
|
279
|
-
|
|
416
|
+
state.themeWarnings =
|
|
417
|
+
resolved.warnings.length > 0 ? ['theme-selection-normalized'] : [];
|
|
418
|
+
state.warnings.push(...state.themeWarnings);
|
|
280
419
|
}
|
|
281
420
|
|
|
282
421
|
async function executeRenderStage(state, options) {
|
|
@@ -341,14 +480,10 @@ async function executeQaStage(state, options, now) {
|
|
|
341
480
|
}
|
|
342
481
|
const warning = reviewGateWarning(reviewError);
|
|
343
482
|
state.reviewGateBlocked = true;
|
|
483
|
+
state.reviewGateReason =
|
|
484
|
+
localEvidenceReason(reviewError) ??
|
|
485
|
+
evidenceReason('browser-review', 'pipeline-failure');
|
|
344
486
|
state.warnings.push(warning);
|
|
345
|
-
state.visualReviewPaths.push(
|
|
346
|
-
...(await writeVisualReviewFailure(state.run, {
|
|
347
|
-
attempt: state.visualReviewAttempt || 1,
|
|
348
|
-
error: reviewError,
|
|
349
|
-
evidence: state.browserEvidence,
|
|
350
|
-
})),
|
|
351
|
-
);
|
|
352
487
|
return {
|
|
353
488
|
outputPaths: [
|
|
354
489
|
...state.rendered.map(({ renderedPath }) => renderedPath),
|
|
@@ -412,10 +547,11 @@ function resolveBrowserProvider(request, recipe, options) {
|
|
|
412
547
|
probe: async (...args) => {
|
|
413
548
|
try {
|
|
414
549
|
return await options.browserProbe(...args);
|
|
415
|
-
} catch
|
|
416
|
-
throw
|
|
417
|
-
'E_VISUAL_REVIEW',
|
|
418
|
-
|
|
550
|
+
} catch {
|
|
551
|
+
throw withEvidenceReason(
|
|
552
|
+
codedError('E_VISUAL_REVIEW', 'Browser evidence callback failed.'),
|
|
553
|
+
'browser-review',
|
|
554
|
+
'provider-failure',
|
|
419
555
|
);
|
|
420
556
|
}
|
|
421
557
|
},
|
|
@@ -429,6 +565,7 @@ async function auditRenderedArtifacts(
|
|
|
429
565
|
now,
|
|
430
566
|
{ browserProvider, htmlSafetyErrors, qaWarnings },
|
|
431
567
|
) {
|
|
568
|
+
await enforceInternalReferenceGate(state, options, now);
|
|
432
569
|
for (const artifact of state.resolvedArtifacts.filter(
|
|
433
570
|
({ authoring }) => authoring === 'html',
|
|
434
571
|
)) {
|
|
@@ -451,22 +588,34 @@ async function auditRenderedArtifacts(
|
|
|
451
588
|
type: artifact.type,
|
|
452
589
|
html: artifact.html,
|
|
453
590
|
}));
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
...(options.widths &&
|
|
463
|
-
!requiresRecapVisualReview(state) && { widths: options.widths }),
|
|
464
|
-
...(browserProvider &&
|
|
465
|
-
requiresRecapVisualReview(state) && {
|
|
466
|
-
evidenceRoot: state.run.runRoot,
|
|
467
|
-
requireBrowserEvidence: true,
|
|
591
|
+
let report;
|
|
592
|
+
try {
|
|
593
|
+
report = await auditArtifactSet({
|
|
594
|
+
artifacts: probeArtifacts,
|
|
595
|
+
setPlan: state.setPlan,
|
|
596
|
+
...(options.denylist && { denylist: options.denylist }),
|
|
597
|
+
...(browserProvider?.session && {
|
|
598
|
+
browserSession: browserProvider.session,
|
|
468
599
|
}),
|
|
469
|
-
|
|
600
|
+
...(browserProvider?.probe && { browserProbe: browserProvider.probe }),
|
|
601
|
+
...(options.widths &&
|
|
602
|
+
!requiresRecapVisualReview(state) && { widths: options.widths }),
|
|
603
|
+
...(browserProvider &&
|
|
604
|
+
requiresRecapVisualReview(state) && {
|
|
605
|
+
evidenceRoot: state.run.runRoot,
|
|
606
|
+
requireBrowserEvidence: true,
|
|
607
|
+
}),
|
|
608
|
+
});
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (requiresRecapVisualReview(state)) {
|
|
611
|
+
throw withEvidenceReason(
|
|
612
|
+
codedError('E_VISUAL_REVIEW', 'Browser review failed.'),
|
|
613
|
+
'browser-review',
|
|
614
|
+
'provider-failure',
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
470
619
|
const hardIssues = report.issues.filter((issue) => isHardQaIssue(issue.code));
|
|
471
620
|
const warningIssues = report.issues.filter(
|
|
472
621
|
(issue) => !isHardQaIssue(issue.code),
|
|
@@ -508,28 +657,50 @@ async function auditRenderedArtifacts(
|
|
|
508
657
|
} else if (visualCritic) {
|
|
509
658
|
await reviewAndRetain(state, visualCritic, 1);
|
|
510
659
|
if (state.visualReview.result.disposition === 'correct') {
|
|
660
|
+
if (state.correctionAttempted) {
|
|
661
|
+
throw codedError(
|
|
662
|
+
'E_VISUAL_CORRECTION',
|
|
663
|
+
'The one bounded artifact correction was already consumed by internal-reference validation.',
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
state.correctionAttempted = true;
|
|
511
667
|
await applyVisualCorrection(state, options, now);
|
|
668
|
+
const correctedReferences = validateRenderedInternalReferences(state);
|
|
669
|
+
if (!correctedReferences.valid) {
|
|
670
|
+
throw internalReferenceError(correctedReferences.errors);
|
|
671
|
+
}
|
|
512
672
|
const correctedArtifacts = state.rendered.map((artifact) => ({
|
|
513
673
|
id: artifact.artifactId,
|
|
514
674
|
type: artifact.type,
|
|
515
675
|
html: artifact.html,
|
|
516
676
|
}));
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
...(options.widths &&
|
|
526
|
-
!requiresRecapVisualReview(state) && { widths: options.widths }),
|
|
527
|
-
...(browserProvider &&
|
|
528
|
-
requiresRecapVisualReview(state) && {
|
|
529
|
-
evidenceRoot: state.run.runRoot,
|
|
530
|
-
requireBrowserEvidence: true,
|
|
677
|
+
let finalReport;
|
|
678
|
+
try {
|
|
679
|
+
finalReport = await auditArtifactSet({
|
|
680
|
+
artifacts: correctedArtifacts,
|
|
681
|
+
setPlan: state.setPlan,
|
|
682
|
+
...(options.denylist && { denylist: options.denylist }),
|
|
683
|
+
...(browserProvider?.session && {
|
|
684
|
+
browserSession: browserProvider.session,
|
|
531
685
|
}),
|
|
532
|
-
|
|
686
|
+
...(browserProvider?.probe && {
|
|
687
|
+
browserProbe: browserProvider.probe,
|
|
688
|
+
}),
|
|
689
|
+
...(options.widths &&
|
|
690
|
+
!requiresRecapVisualReview(state) && { widths: options.widths }),
|
|
691
|
+
...(browserProvider &&
|
|
692
|
+
requiresRecapVisualReview(state) && {
|
|
693
|
+
evidenceRoot: state.run.runRoot,
|
|
694
|
+
requireBrowserEvidence: true,
|
|
695
|
+
}),
|
|
696
|
+
});
|
|
697
|
+
} catch {
|
|
698
|
+
throw withEvidenceReason(
|
|
699
|
+
codedError('E_VISUAL_REVIEW', 'Corrected browser review failed.'),
|
|
700
|
+
'browser-review',
|
|
701
|
+
'provider-failure',
|
|
702
|
+
);
|
|
703
|
+
}
|
|
533
704
|
const finalHardIssues = finalReport.issues.filter((issue) =>
|
|
534
705
|
isHardQaIssue(issue.code),
|
|
535
706
|
);
|
|
@@ -586,6 +757,162 @@ async function auditRenderedArtifacts(
|
|
|
586
757
|
};
|
|
587
758
|
}
|
|
588
759
|
|
|
760
|
+
async function enforceInternalReferenceGate(state, options, now) {
|
|
761
|
+
const validate = () => validateRenderedInternalReferences(state);
|
|
762
|
+
const initial = validate();
|
|
763
|
+
if (initial.valid) return;
|
|
764
|
+
|
|
765
|
+
const correctionAuthor = options.correctArtifact ?? options.author;
|
|
766
|
+
if (typeof correctionAuthor !== 'function') {
|
|
767
|
+
throw internalReferenceError(initial.errors);
|
|
768
|
+
}
|
|
769
|
+
const trust = authorTrustContext(options, now);
|
|
770
|
+
const artifactIds = [
|
|
771
|
+
...new Set(
|
|
772
|
+
initial.errors
|
|
773
|
+
.map(({ artifactId }) => artifactId)
|
|
774
|
+
.filter((artifactId) => typeof artifactId === 'string'),
|
|
775
|
+
),
|
|
776
|
+
];
|
|
777
|
+
if (artifactIds.length === 0) {
|
|
778
|
+
throw internalReferenceError(initial.errors);
|
|
779
|
+
}
|
|
780
|
+
state.correctionAttempted = true;
|
|
781
|
+
for (const artifactId of artifactIds) {
|
|
782
|
+
const artifactIndex = state.resolvedArtifacts.findIndex(
|
|
783
|
+
({ id }) => id === artifactId,
|
|
784
|
+
);
|
|
785
|
+
if (artifactIndex < 0) {
|
|
786
|
+
throw internalReferenceError(initial.errors);
|
|
787
|
+
}
|
|
788
|
+
const artifact = state.resolvedArtifacts[artifactIndex];
|
|
789
|
+
let item;
|
|
790
|
+
try {
|
|
791
|
+
item = await authorArtifact(
|
|
792
|
+
state,
|
|
793
|
+
artifact,
|
|
794
|
+
correctionAuthor,
|
|
795
|
+
trust,
|
|
796
|
+
canonicalArtifactLinks(
|
|
797
|
+
state.resolvedArtifacts,
|
|
798
|
+
artifact.id,
|
|
799
|
+
state.run.slug,
|
|
800
|
+
),
|
|
801
|
+
{
|
|
802
|
+
attempt: 1,
|
|
803
|
+
reason: 'internal-reference-validation',
|
|
804
|
+
findings: structuredClone(
|
|
805
|
+
initial.errors.filter(
|
|
806
|
+
({ artifactId: findingArtifactId }) =>
|
|
807
|
+
findingArtifactId === artifactId,
|
|
808
|
+
),
|
|
809
|
+
),
|
|
810
|
+
previousContentPath: state.contentPaths.get(artifactId),
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
throw withEvidenceReason(
|
|
815
|
+
codedError(
|
|
816
|
+
'E_INTERNAL_REFERENCE',
|
|
817
|
+
`Internal-reference correction failed for ${artifactId}: ${safeMessage(error)}`,
|
|
818
|
+
),
|
|
819
|
+
'link-validation',
|
|
820
|
+
'pipeline-failure',
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
await installCorrectedArtifact(state, artifactIndex, item);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const final = validate();
|
|
827
|
+
if (!final.valid) {
|
|
828
|
+
throw internalReferenceError(final.errors);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function validateRenderedInternalReferences(state) {
|
|
833
|
+
return validateInternalReferences({
|
|
834
|
+
artifacts: state.rendered.map((artifact) => ({
|
|
835
|
+
artifactId: artifact.artifactId,
|
|
836
|
+
renderedPath: artifact.renderedPath,
|
|
837
|
+
html: artifact.html,
|
|
838
|
+
})),
|
|
839
|
+
manifestPaths: state.artifacts.map(({ renderedPath }) => renderedPath),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function installCorrectedArtifact(state, artifactIndex, item) {
|
|
844
|
+
const artifactId = item.artifact.id;
|
|
845
|
+
await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
|
|
846
|
+
await writeTextAtomic(state.run.runRoot, item.contentPath, item.content);
|
|
847
|
+
state.resolvedArtifacts[artifactIndex] = item.artifact;
|
|
848
|
+
state.authoredContent.set(artifactId, item.content);
|
|
849
|
+
state.contentPaths.set(artifactId, item.contentPath);
|
|
850
|
+
if (item.artifact.authoring === 'markdown') {
|
|
851
|
+
const links = expansionLinks(state.resolvedArtifacts);
|
|
852
|
+
const model = assertValidContentModel(
|
|
853
|
+
state.recipe,
|
|
854
|
+
markdownContentModel(
|
|
855
|
+
item.artifact,
|
|
856
|
+
state.run.slug,
|
|
857
|
+
item.content,
|
|
858
|
+
item.artifact.origin === 'floor' ? links : [],
|
|
859
|
+
),
|
|
860
|
+
item.artifact,
|
|
861
|
+
);
|
|
862
|
+
const modelIndex = state.contentModels.findIndex(
|
|
863
|
+
({ artifactId: id }) => id === artifactId,
|
|
864
|
+
);
|
|
865
|
+
state.contentModels[modelIndex] = model;
|
|
866
|
+
}
|
|
867
|
+
const rendered =
|
|
868
|
+
item.artifact.authoring === 'markdown'
|
|
869
|
+
? await renderArtifact({
|
|
870
|
+
recipeArtifact: renderDescriptor(item.artifact),
|
|
871
|
+
content: state.contentModels.find(
|
|
872
|
+
({ artifactId: id }) => id === artifactId,
|
|
873
|
+
),
|
|
874
|
+
factBase: state.factBase,
|
|
875
|
+
theme: state.theme,
|
|
876
|
+
renderStrategy: state.renderStrategy,
|
|
877
|
+
...(state.run.request.publicBaseUrl && {
|
|
878
|
+
publicBaseUrl: state.run.request.publicBaseUrl,
|
|
879
|
+
}),
|
|
880
|
+
})
|
|
881
|
+
: artisticRender(state, item.artifact);
|
|
882
|
+
await writeTextAtomic(
|
|
883
|
+
state.run.runRoot,
|
|
884
|
+
rendered.renderedPath,
|
|
885
|
+
rendered.html,
|
|
886
|
+
);
|
|
887
|
+
const renderedIndex = state.rendered.findIndex(
|
|
888
|
+
({ artifactId: id }) => id === artifactId,
|
|
889
|
+
);
|
|
890
|
+
state.rendered[renderedIndex] = rendered;
|
|
891
|
+
state.artifacts[renderedIndex] = artifactRecord(state, rendered);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function internalReferenceError(errors) {
|
|
895
|
+
// `link-validation` is one of the eight closed evidence stages and is
|
|
896
|
+
// accepted by both evidence schemas, but the gate runs inside the `qa` build
|
|
897
|
+
// stage, and `evidenceStageForBuildStage` maps `qa -> browser-review`. Without
|
|
898
|
+
// an explicit local reason a broken internal link is durably recorded as a
|
|
899
|
+
// browser-review failure, which is the one thing the closed stage enum exists
|
|
900
|
+
// to prevent. Fail-closed behavior is unchanged; only attribution improves.
|
|
901
|
+
return withEvidenceReason(
|
|
902
|
+
codedError(
|
|
903
|
+
'E_INTERNAL_REFERENCE',
|
|
904
|
+
errors
|
|
905
|
+
.map(
|
|
906
|
+
({ code, renderedPath, reference, message }) =>
|
|
907
|
+
`${code}: ${renderedPath ?? 'site'}${reference ? ` references ${reference}` : ''}: ${message}`,
|
|
908
|
+
)
|
|
909
|
+
.join('; '),
|
|
910
|
+
),
|
|
911
|
+
'link-validation',
|
|
912
|
+
'pipeline-failure',
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
589
916
|
function resolveVisualCritic(options) {
|
|
590
917
|
if (options.visualCritic === undefined) return null;
|
|
591
918
|
if (typeof options.visualCritic !== 'function') {
|
|
@@ -616,13 +943,21 @@ function requiresRecapVisualReview(state) {
|
|
|
616
943
|
async function reviewAndRetain(state, visualCritic, attempt) {
|
|
617
944
|
state.visualReviewAttempt = attempt;
|
|
618
945
|
try {
|
|
619
|
-
|
|
946
|
+
const review = await runVisualReview({
|
|
620
947
|
plan: state.setPlan,
|
|
621
948
|
rendered: state.rendered,
|
|
622
949
|
evidence: state.browserEvidence,
|
|
623
950
|
visualCritic,
|
|
624
951
|
runRoot: state.run.runRoot,
|
|
625
952
|
});
|
|
953
|
+
state.visualReview = {
|
|
954
|
+
...review,
|
|
955
|
+
evidence: createVisualReviewEvidence({
|
|
956
|
+
request: review.request,
|
|
957
|
+
attempt,
|
|
958
|
+
result: review.result,
|
|
959
|
+
}),
|
|
960
|
+
};
|
|
626
961
|
state.visualReviewPaths.push(
|
|
627
962
|
...(await writeVisualReviewAttempt(state.run, {
|
|
628
963
|
attempt,
|
|
@@ -630,7 +965,39 @@ async function reviewAndRetain(state, visualCritic, attempt) {
|
|
|
630
965
|
})),
|
|
631
966
|
);
|
|
632
967
|
} catch (error) {
|
|
633
|
-
|
|
968
|
+
if (error?.visualReviewRequest) {
|
|
969
|
+
const kind = ['provider-failure', 'pipeline-failure'].includes(
|
|
970
|
+
error.evidenceKind,
|
|
971
|
+
)
|
|
972
|
+
? error.evidenceKind
|
|
973
|
+
: 'pipeline-failure';
|
|
974
|
+
const evidence = createVisualReviewEvidence({
|
|
975
|
+
request: error.visualReviewRequest,
|
|
976
|
+
attempt,
|
|
977
|
+
failureKind: kind,
|
|
978
|
+
});
|
|
979
|
+
state.visualReview = {
|
|
980
|
+
request: structuredClone(error.visualReviewRequest),
|
|
981
|
+
evidence,
|
|
982
|
+
};
|
|
983
|
+
state.visualReviewPaths.push(
|
|
984
|
+
...(await writeVisualReviewFailure(state.run, {
|
|
985
|
+
attempt,
|
|
986
|
+
request: error.visualReviewRequest,
|
|
987
|
+
kind,
|
|
988
|
+
})),
|
|
989
|
+
);
|
|
990
|
+
throw withEvidenceReason(
|
|
991
|
+
codedError('E_VISUAL_REVIEW', 'Visual review failed.'),
|
|
992
|
+
'visual-review',
|
|
993
|
+
kind,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
throw withEvidenceReason(
|
|
997
|
+
codedError('E_VISUAL_REVIEW', 'Visual review pipeline failed.'),
|
|
998
|
+
'visual-review',
|
|
999
|
+
'pipeline-failure',
|
|
1000
|
+
);
|
|
634
1001
|
}
|
|
635
1002
|
}
|
|
636
1003
|
|
|
@@ -662,17 +1029,29 @@ async function applyVisualCorrection(state, options, now) {
|
|
|
662
1029
|
const previousContent = state.authoredContent.get(artifactId);
|
|
663
1030
|
let item;
|
|
664
1031
|
try {
|
|
665
|
-
item = await authorArtifact(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1032
|
+
item = await authorArtifact(
|
|
1033
|
+
state,
|
|
1034
|
+
artifact,
|
|
1035
|
+
correctionAuthor,
|
|
1036
|
+
trust,
|
|
1037
|
+
canonicalArtifactLinks(
|
|
1038
|
+
state.resolvedArtifacts,
|
|
1039
|
+
artifact.id,
|
|
1040
|
+
state.run.slug,
|
|
669
1041
|
),
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1042
|
+
{
|
|
1043
|
+
attempt: 1,
|
|
1044
|
+
findings: structuredClone(
|
|
1045
|
+
findings.filter((finding) => finding.artifactId === artifactId),
|
|
1046
|
+
),
|
|
1047
|
+
previousContentPath: state.contentPaths.get(artifactId),
|
|
1048
|
+
},
|
|
1049
|
+
);
|
|
1050
|
+
} catch {
|
|
1051
|
+
throw withEvidenceReason(
|
|
1052
|
+
codedError('E_VISUAL_CORRECTION', 'Visual correction provider failed.'),
|
|
1053
|
+
'visual-review',
|
|
1054
|
+
'provider-failure',
|
|
676
1055
|
);
|
|
677
1056
|
}
|
|
678
1057
|
if ((item.result.proposedArtifacts ?? []).length > 0) {
|
|
@@ -772,9 +1151,9 @@ function isReviewGateError(error) {
|
|
|
772
1151
|
|
|
773
1152
|
function normalizeReviewGateError(state, error) {
|
|
774
1153
|
if (!requiresRecapVisualReview(state)) return null;
|
|
775
|
-
if (isReviewGateError(error))
|
|
776
|
-
|
|
777
|
-
return
|
|
1154
|
+
if (isReviewGateError(error)) {
|
|
1155
|
+
if (localEvidenceReason(error)) return error;
|
|
1156
|
+
return withEvidenceReason(error, 'visual-review', 'pipeline-failure');
|
|
778
1157
|
}
|
|
779
1158
|
return null;
|
|
780
1159
|
}
|
|
@@ -783,8 +1162,10 @@ function reviewGateWarning(error) {
|
|
|
783
1162
|
const reason =
|
|
784
1163
|
error?.code === 'E_VISUAL_CORRECTION'
|
|
785
1164
|
? 'correction-failed'
|
|
786
|
-
: 'review
|
|
787
|
-
|
|
1165
|
+
: localEvidenceReason(error)?.stage === 'browser-review'
|
|
1166
|
+
? 'browser-review-failed'
|
|
1167
|
+
: 'review-chain-failed';
|
|
1168
|
+
return `visual-review-required:${reason}`;
|
|
788
1169
|
}
|
|
789
1170
|
|
|
790
1171
|
function artisticRender(state, artifact) {
|
|
@@ -1124,23 +1505,22 @@ async function readJson(path) {
|
|
|
1124
1505
|
export async function runExplainerCli(
|
|
1125
1506
|
argv = process.argv.slice(2),
|
|
1126
1507
|
io = console,
|
|
1508
|
+
run = runExplainer,
|
|
1127
1509
|
) {
|
|
1128
1510
|
try {
|
|
1129
1511
|
const parsed = await parseCli(argv);
|
|
1130
1512
|
const request = JSON.parse(await readFile(parsed.requestPath, 'utf8'));
|
|
1131
|
-
const result = await
|
|
1132
|
-
io.log(JSON.stringify(result, null, 2));
|
|
1513
|
+
const result = await run(request, parsed.options);
|
|
1514
|
+
io.log(JSON.stringify(projectCliRunResult(result), null, 2));
|
|
1133
1515
|
return result.outcome === 'failed' ? 1 : 0;
|
|
1134
1516
|
} catch (error) {
|
|
1135
|
-
io.
|
|
1517
|
+
io.error(
|
|
1136
1518
|
JSON.stringify(
|
|
1137
1519
|
{
|
|
1138
1520
|
outcome: 'failed',
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
message: safeMessage(error),
|
|
1143
|
-
},
|
|
1521
|
+
reasons: [
|
|
1522
|
+
localEvidenceReason(error) ??
|
|
1523
|
+
evidenceReason('planning', 'pipeline-failure'),
|
|
1144
1524
|
],
|
|
1145
1525
|
},
|
|
1146
1526
|
null,
|
|
@@ -1151,6 +1531,191 @@ export async function runExplainerCli(
|
|
|
1151
1531
|
}
|
|
1152
1532
|
}
|
|
1153
1533
|
|
|
1534
|
+
function projectCliRunResult(result) {
|
|
1535
|
+
if (!isObject(result)) {
|
|
1536
|
+
return {
|
|
1537
|
+
outcome: 'failed',
|
|
1538
|
+
reasons: [evidenceReason('finalization', 'pipeline-failure')],
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
const projected = {};
|
|
1542
|
+
for (const key of [
|
|
1543
|
+
'runId',
|
|
1544
|
+
'runRoot',
|
|
1545
|
+
'manifestPath',
|
|
1546
|
+
'buildRecordPath',
|
|
1547
|
+
'outcome',
|
|
1548
|
+
'marking',
|
|
1549
|
+
]) {
|
|
1550
|
+
if (typeof result[key] === 'string') projected[key] = result[key];
|
|
1551
|
+
}
|
|
1552
|
+
if (Array.isArray(result.warnings)) {
|
|
1553
|
+
projected.warnings = retainedWarnings(result.warnings);
|
|
1554
|
+
}
|
|
1555
|
+
if (isObject(result.discovery)) {
|
|
1556
|
+
projected.discovery = {
|
|
1557
|
+
...(Number.isInteger(result.discovery.rounds) && {
|
|
1558
|
+
rounds: result.discovery.rounds,
|
|
1559
|
+
}),
|
|
1560
|
+
...(Number.isInteger(result.discovery.findingCount) && {
|
|
1561
|
+
findingCount: result.discovery.findingCount,
|
|
1562
|
+
}),
|
|
1563
|
+
...(['not-requested', 'two-empty-rounds', 'hard-maximum'].includes(
|
|
1564
|
+
result.discovery.reason,
|
|
1565
|
+
) && { reason: result.discovery.reason }),
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
if (isObject(result.approval)) {
|
|
1569
|
+
projected.approval = pickStringFields(result.approval, [
|
|
1570
|
+
'status',
|
|
1571
|
+
'path',
|
|
1572
|
+
'marking',
|
|
1573
|
+
'resumeToken',
|
|
1574
|
+
]);
|
|
1575
|
+
}
|
|
1576
|
+
const reasons = projectEvidenceReasons(result.reasons);
|
|
1577
|
+
if (reasons.length > 0) projected.reasons = reasons;
|
|
1578
|
+
const visualReview = projectVisualReviewEvidence(result.visualReview);
|
|
1579
|
+
if (visualReview) projected.visualReview = visualReview;
|
|
1580
|
+
const publication = projectPublicationSummaryForCli(result.publication);
|
|
1581
|
+
if (publication) projected.publication = publication;
|
|
1582
|
+
return projected;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function projectVisualReviewEvidence(value) {
|
|
1586
|
+
if (!isObject(value)) return null;
|
|
1587
|
+
const reasons = projectEvidenceReasons(value.reasons);
|
|
1588
|
+
if (
|
|
1589
|
+
value.schemaVersion !== 'explainer-kit.visual-review-evidence/v1' ||
|
|
1590
|
+
typeof value.requestHash !== 'string' ||
|
|
1591
|
+
![1, 2].includes(value.attempt) ||
|
|
1592
|
+
!['pass', 'correct', 'failed'].includes(value.disposition) ||
|
|
1593
|
+
!Array.isArray(value.reasons) ||
|
|
1594
|
+
reasons.length !== value.reasons.length
|
|
1595
|
+
) {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
schemaVersion: value.schemaVersion,
|
|
1600
|
+
requestHash: value.requestHash,
|
|
1601
|
+
attempt: value.attempt,
|
|
1602
|
+
disposition: value.disposition,
|
|
1603
|
+
reasons,
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function projectPublicationSummaryForCli(value) {
|
|
1608
|
+
if (!isObject(value)) return null;
|
|
1609
|
+
if (
|
|
1610
|
+
value.schemaVersion !== 'explainer-kit.publish-summary/v1' &&
|
|
1611
|
+
value.schemaVersion !== 'explainer-kit.publish-summary/v2'
|
|
1612
|
+
) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
schemaVersion: value.schemaVersion,
|
|
1617
|
+
...(typeof value.receiptSchemaVersion === 'string' && {
|
|
1618
|
+
receiptSchemaVersion: value.receiptSchemaVersion,
|
|
1619
|
+
}),
|
|
1620
|
+
...(typeof value.publicAccess === 'string' && {
|
|
1621
|
+
publicAccess: value.publicAccess,
|
|
1622
|
+
}),
|
|
1623
|
+
...(Array.isArray(value.artifacts) && {
|
|
1624
|
+
artifacts: value.artifacts.map((artifact) =>
|
|
1625
|
+
isObject(artifact)
|
|
1626
|
+
? {
|
|
1627
|
+
...pickStringFields(artifact, [
|
|
1628
|
+
'relativePath',
|
|
1629
|
+
'publicUrl',
|
|
1630
|
+
's3Uri',
|
|
1631
|
+
'hash',
|
|
1632
|
+
'contentType',
|
|
1633
|
+
]),
|
|
1634
|
+
...(isObject(artifact.source) && {
|
|
1635
|
+
source: pickStringFields(artifact.source, [
|
|
1636
|
+
'kind',
|
|
1637
|
+
'artifactId',
|
|
1638
|
+
'name',
|
|
1639
|
+
]),
|
|
1640
|
+
}),
|
|
1641
|
+
...(isObject(artifact.objectVerification) && {
|
|
1642
|
+
objectVerification: pickClosedVerification(
|
|
1643
|
+
artifact.objectVerification,
|
|
1644
|
+
),
|
|
1645
|
+
}),
|
|
1646
|
+
...(isObject(artifact.publicVerification) && {
|
|
1647
|
+
publicVerification: pickClosedVerification(
|
|
1648
|
+
artifact.publicVerification,
|
|
1649
|
+
),
|
|
1650
|
+
}),
|
|
1651
|
+
}
|
|
1652
|
+
: {},
|
|
1653
|
+
),
|
|
1654
|
+
}),
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function pickClosedVerification(value) {
|
|
1659
|
+
return {
|
|
1660
|
+
...pickStringFields(value, ['status', 'method', 'hash']),
|
|
1661
|
+
...(Number.isInteger(value.httpStatus) && {
|
|
1662
|
+
httpStatus: value.httpStatus,
|
|
1663
|
+
}),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function projectEvidenceReasons(value) {
|
|
1668
|
+
if (!Array.isArray(value)) return [];
|
|
1669
|
+
return value.flatMap((reason) => {
|
|
1670
|
+
if (
|
|
1671
|
+
!isObject(reason) ||
|
|
1672
|
+
![
|
|
1673
|
+
'planning',
|
|
1674
|
+
'authoring',
|
|
1675
|
+
'rendering',
|
|
1676
|
+
'link-validation',
|
|
1677
|
+
'browser-review',
|
|
1678
|
+
'visual-review',
|
|
1679
|
+
'durability',
|
|
1680
|
+
'finalization',
|
|
1681
|
+
].includes(reason.stage) ||
|
|
1682
|
+
![
|
|
1683
|
+
'finding',
|
|
1684
|
+
'provider-failure',
|
|
1685
|
+
'pipeline-failure',
|
|
1686
|
+
'superseded',
|
|
1687
|
+
].includes(reason.kind) ||
|
|
1688
|
+
!Number.isInteger(reason.count) ||
|
|
1689
|
+
reason.count < 1 ||
|
|
1690
|
+
reason.count > 50
|
|
1691
|
+
) {
|
|
1692
|
+
return [];
|
|
1693
|
+
}
|
|
1694
|
+
return [
|
|
1695
|
+
{
|
|
1696
|
+
stage: reason.stage,
|
|
1697
|
+
kind: reason.kind,
|
|
1698
|
+
...(typeof reason.artifactId === 'string' && {
|
|
1699
|
+
artifactId: reason.artifactId,
|
|
1700
|
+
}),
|
|
1701
|
+
count: reason.count,
|
|
1702
|
+
},
|
|
1703
|
+
];
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function pickStringFields(value, keys) {
|
|
1708
|
+
return Object.fromEntries(
|
|
1709
|
+
keys.flatMap((key) =>
|
|
1710
|
+
typeof value[key] === 'string' ? [[key, value[key]]] : [],
|
|
1711
|
+
),
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function isObject(value) {
|
|
1716
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1154
1719
|
async function executeStage(run, id, options, operation) {
|
|
1155
1720
|
await updateBuildRecord(run, { id, status: 'running' });
|
|
1156
1721
|
try {
|
|
@@ -1164,18 +1729,19 @@ async function executeStage(run, id, options, operation) {
|
|
|
1164
1729
|
...(result.warnings !== undefined && { warnings: result.warnings }),
|
|
1165
1730
|
});
|
|
1166
1731
|
} catch (error) {
|
|
1732
|
+
const projected =
|
|
1733
|
+
localEvidenceReason(error) ??
|
|
1734
|
+
evidenceReason(evidenceStageForBuildStage(id), 'pipeline-failure');
|
|
1167
1735
|
await updateBuildRecord(run, {
|
|
1168
1736
|
id,
|
|
1169
1737
|
status: 'failed',
|
|
1170
|
-
error:
|
|
1171
|
-
code: error.code ?? stageErrorCode(id),
|
|
1172
|
-
message: safeMessage(error),
|
|
1173
|
-
recovery: [
|
|
1174
|
-
`Correct the ${id} inputs or implementation and start a new run.`,
|
|
1175
|
-
],
|
|
1176
|
-
},
|
|
1738
|
+
error: true,
|
|
1177
1739
|
});
|
|
1178
|
-
throw
|
|
1740
|
+
throw withEvidenceReason(
|
|
1741
|
+
codedError(stageErrorCode(id), `The ${id} stage failed.`),
|
|
1742
|
+
projected.stage,
|
|
1743
|
+
projected.kind,
|
|
1744
|
+
);
|
|
1179
1745
|
}
|
|
1180
1746
|
}
|
|
1181
1747
|
|
|
@@ -1281,17 +1847,41 @@ async function executeDurabilityAndPublish(state, options, now) {
|
|
|
1281
1847
|
'Commit durability was requested without a durability callback.',
|
|
1282
1848
|
);
|
|
1283
1849
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1850
|
+
let providerFailed = false;
|
|
1851
|
+
try {
|
|
1852
|
+
await options.durability({
|
|
1853
|
+
runRoot: state.run.runRoot,
|
|
1854
|
+
manifestPath: state.run.manifestPath,
|
|
1855
|
+
buildRecordPath: state.run.buildRecordPath,
|
|
1856
|
+
});
|
|
1857
|
+
} catch {
|
|
1858
|
+
providerFailed = true;
|
|
1859
|
+
}
|
|
1860
|
+
let inventoryFailed = false;
|
|
1861
|
+
try {
|
|
1862
|
+
const manifest = JSON.parse(
|
|
1863
|
+
await readFile(state.run.manifestPath, 'utf8'),
|
|
1864
|
+
);
|
|
1865
|
+
await enforceRetainedRunPackage(state, manifest);
|
|
1866
|
+
} catch {
|
|
1867
|
+
inventoryFailed = true;
|
|
1868
|
+
}
|
|
1869
|
+
if (providerFailed || inventoryFailed) {
|
|
1870
|
+
await updateBuildRecord(state.run, {
|
|
1871
|
+
id: 'durability',
|
|
1872
|
+
status: 'failed',
|
|
1873
|
+
error: true,
|
|
1874
|
+
});
|
|
1875
|
+
throw withEvidenceReason(
|
|
1876
|
+
codedError('E_DURABILITY', 'Durability provider failed.'),
|
|
1877
|
+
'durability',
|
|
1878
|
+
providerFailed ? 'provider-failure' : 'pipeline-failure',
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1289
1881
|
await updateBuildRecord(state.run, {
|
|
1290
1882
|
id: 'durability',
|
|
1291
1883
|
status: 'warned',
|
|
1292
|
-
warnings: [
|
|
1293
|
-
'Commit durability requires caller-created evidence through record-durability.mjs.',
|
|
1294
|
-
],
|
|
1884
|
+
warnings: ['durability-evidence-required'],
|
|
1295
1885
|
});
|
|
1296
1886
|
await updateBuildRecord(state.run, { id: 'publish', status: 'skipped' });
|
|
1297
1887
|
return false;
|
|
@@ -1305,58 +1895,164 @@ async function executeDurabilityAndPublish(state, options, now) {
|
|
|
1305
1895
|
'Publish durability was requested without an explicit publisher callback.',
|
|
1306
1896
|
);
|
|
1307
1897
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
'Publishing requires separately retained verified receipt evidence.',
|
|
1313
|
-
],
|
|
1314
|
-
});
|
|
1315
|
-
await persistManifest(state, now());
|
|
1898
|
+
const finalizedAt = now();
|
|
1899
|
+
const finalizedManifest = await persistManifest(state, finalizedAt);
|
|
1900
|
+
let providerFailed = false;
|
|
1901
|
+
let publishedReceipt;
|
|
1316
1902
|
try {
|
|
1317
|
-
|
|
1903
|
+
const buildRecord = JSON.parse(
|
|
1904
|
+
await readFile(state.run.buildRecordPath, 'utf8'),
|
|
1905
|
+
);
|
|
1906
|
+
assertManifestPublishable(finalizedManifest, { buildRecord });
|
|
1907
|
+
const receipt = await options.publish({
|
|
1318
1908
|
request: structuredClone(state.run.request.durability.publish),
|
|
1319
1909
|
runRoot: state.run.runRoot,
|
|
1320
1910
|
manifestPath: state.run.manifestPath,
|
|
1321
1911
|
});
|
|
1322
|
-
|
|
1912
|
+
const schemaValidation = validateContract('publish-receipt', receipt);
|
|
1913
|
+
if (!schemaValidation.valid) {
|
|
1914
|
+
throw codedError(
|
|
1915
|
+
'E_PUBLISH',
|
|
1916
|
+
`Publisher returned an invalid receipt: ${schemaValidation.errors[0].message}`,
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
const crossRecordValidation = validateContract(
|
|
1920
|
+
'publish-receipt',
|
|
1921
|
+
receipt,
|
|
1922
|
+
publicationValidationContext(receipt, finalizedManifest),
|
|
1923
|
+
);
|
|
1924
|
+
if (!crossRecordValidation.valid) {
|
|
1925
|
+
throw codedError(
|
|
1926
|
+
'E_PUBLISH',
|
|
1927
|
+
`Publisher returned an invalid receipt: ${crossRecordValidation.errors[0].message}`,
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
publishedReceipt = receipt;
|
|
1931
|
+
} catch {
|
|
1932
|
+
providerFailed = true;
|
|
1933
|
+
}
|
|
1934
|
+
// Everything below the provider boundary is local work. Folding it into the
|
|
1935
|
+
// try above classified a failed local write, build-record update or manifest
|
|
1936
|
+
// rewrite as `provider-failure`, blaming the destination for a defect on this
|
|
1937
|
+
// machine.
|
|
1938
|
+
let pipelineFailed = false;
|
|
1939
|
+
if (!providerFailed) {
|
|
1940
|
+
try {
|
|
1941
|
+
state.publishReceiptPath = 'publish-receipt.json';
|
|
1942
|
+
await writeJsonAtomic(
|
|
1943
|
+
state.run.runRoot,
|
|
1944
|
+
state.publishReceiptPath,
|
|
1945
|
+
publishedReceipt,
|
|
1946
|
+
);
|
|
1947
|
+
state.publication = publicationSummary(publishedReceipt);
|
|
1948
|
+
await updateBuildRecord(state.run, {
|
|
1949
|
+
id: 'publish',
|
|
1950
|
+
status: 'warned',
|
|
1951
|
+
warnings: ['publish-receipt-evidence-required'],
|
|
1952
|
+
});
|
|
1953
|
+
await persistManifest(state, finalizedAt);
|
|
1954
|
+
} catch {
|
|
1955
|
+
pipelineFailed = true;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
let inventoryFailed = false;
|
|
1959
|
+
try {
|
|
1960
|
+
const manifest = JSON.parse(await readFile(state.run.manifestPath, 'utf8'));
|
|
1961
|
+
await enforceRetainedRunPackage(state, manifest);
|
|
1962
|
+
} catch {
|
|
1963
|
+
inventoryFailed = true;
|
|
1964
|
+
}
|
|
1965
|
+
if (providerFailed || pipelineFailed || inventoryFailed) {
|
|
1323
1966
|
await updateBuildRecord(state.run, {
|
|
1324
1967
|
id: 'publish',
|
|
1325
1968
|
status: 'failed',
|
|
1326
|
-
error:
|
|
1327
|
-
code: error.code ?? 'E_PUBLISH',
|
|
1328
|
-
message: safeMessage(error),
|
|
1329
|
-
recovery: ['Correct the publish failure and start a new run.'],
|
|
1330
|
-
},
|
|
1969
|
+
error: true,
|
|
1331
1970
|
});
|
|
1332
|
-
throw
|
|
1971
|
+
throw withEvidenceReason(
|
|
1972
|
+
codedError(
|
|
1973
|
+
'E_PUBLISH',
|
|
1974
|
+
providerFailed
|
|
1975
|
+
? 'Publication provider failed.'
|
|
1976
|
+
: 'Publication failed after upload while finalizing local records.',
|
|
1977
|
+
),
|
|
1978
|
+
'durability',
|
|
1979
|
+
providerFailed ? 'provider-failure' : 'pipeline-failure',
|
|
1980
|
+
);
|
|
1333
1981
|
}
|
|
1334
1982
|
return true;
|
|
1335
1983
|
}
|
|
1336
1984
|
|
|
1337
1985
|
async function persistManifest(state, createdAt) {
|
|
1338
1986
|
const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
record,
|
|
1342
|
-
createdAt,
|
|
1343
|
-
await immutableHashesFor(state),
|
|
1344
|
-
);
|
|
1345
|
-
await writeManifestAtomic(state.run, manifest);
|
|
1987
|
+
const immutableHashes = await immutableHashesFor(state);
|
|
1988
|
+
let manifest = manifestFor(state, record, createdAt, immutableHashes);
|
|
1346
1989
|
const publicBaseUrl =
|
|
1347
1990
|
state.run.request.publicBaseUrl ??
|
|
1348
1991
|
state.run.request.durability?.publish?.publicBaseUrl;
|
|
1349
1992
|
if (publicBaseUrl) {
|
|
1993
|
+
// Must match the catalog the connector builds and uploads byte for byte,
|
|
1994
|
+
// so the verification policy is resolved from the same request fields.
|
|
1995
|
+
const publicAccess =
|
|
1996
|
+
state.run.request.publicAccess ??
|
|
1997
|
+
state.run.request.durability?.publish?.publicAccess;
|
|
1998
|
+
const catalogPath = initiativeCatalogPath(manifest.slug);
|
|
1350
1999
|
await writeJsonAtomic(
|
|
1351
2000
|
state.run.runRoot,
|
|
1352
|
-
|
|
1353
|
-
catalogFromManifest(manifest, publicBaseUrl),
|
|
2001
|
+
catalogPath,
|
|
2002
|
+
catalogFromManifest(manifest, publicBaseUrl, { publicAccess }),
|
|
1354
2003
|
);
|
|
2004
|
+
manifest = manifestFor(state, record, createdAt, {
|
|
2005
|
+
...immutableHashes,
|
|
2006
|
+
[catalogPath]: hashBytes(
|
|
2007
|
+
await readFile(join(state.run.runRoot, catalogPath)),
|
|
2008
|
+
),
|
|
2009
|
+
});
|
|
1355
2010
|
}
|
|
2011
|
+
await writeManifestAtomic(state.run, manifest);
|
|
1356
2012
|
return manifest;
|
|
1357
2013
|
}
|
|
1358
2014
|
|
|
1359
|
-
async function
|
|
2015
|
+
async function inventoryManifestFor(state, createdAt) {
|
|
2016
|
+
const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
|
|
2017
|
+
return manifestFor(state, record, createdAt, await immutableHashesFor(state));
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function enforceRetainedRunPackage(
|
|
2021
|
+
state,
|
|
2022
|
+
manifest,
|
|
2023
|
+
{
|
|
2024
|
+
acceptCleanedUnexpected = false,
|
|
2025
|
+
failureStage,
|
|
2026
|
+
includeManifest = true,
|
|
2027
|
+
includeTerminalEvidence = false,
|
|
2028
|
+
} = {},
|
|
2029
|
+
) {
|
|
2030
|
+
try {
|
|
2031
|
+
return await enforceRunPackageInventory(state.run.runRoot, manifest, {
|
|
2032
|
+
includeManifest,
|
|
2033
|
+
includeTerminalEvidence,
|
|
2034
|
+
removeUnexpected: true,
|
|
2035
|
+
});
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
if (acceptCleanedUnexpected) {
|
|
2038
|
+
await enforceRunPackageInventory(state.run.runRoot, manifest, {
|
|
2039
|
+
includeManifest,
|
|
2040
|
+
includeTerminalEvidence,
|
|
2041
|
+
});
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (failureStage) {
|
|
2045
|
+
await updateBuildRecord(state.run, {
|
|
2046
|
+
id: failureStage,
|
|
2047
|
+
status: 'failed',
|
|
2048
|
+
error: true,
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async function persistFailureManifest(state, _error, createdAt) {
|
|
1360
2056
|
const record = JSON.parse(await readFile(state.run.buildRecordPath, 'utf8'));
|
|
1361
2057
|
const recordedIds = new Set(state.artifacts.map(({ id }) => id));
|
|
1362
2058
|
state.artifacts.push(
|
|
@@ -1369,8 +2065,8 @@ async function persistFailureManifest(state, error, createdAt) {
|
|
|
1369
2065
|
status: 'failed',
|
|
1370
2066
|
rebuildable: false,
|
|
1371
2067
|
failure: {
|
|
1372
|
-
code:
|
|
1373
|
-
message:
|
|
2068
|
+
code: 'E_ARTIFACT_BUILD',
|
|
2069
|
+
message: 'Artifact construction failed.',
|
|
1374
2070
|
recovery: ['Correct the failed stage and start a new run.'],
|
|
1375
2071
|
},
|
|
1376
2072
|
})),
|
|
@@ -1412,7 +2108,7 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
|
|
|
1412
2108
|
path: 'build-record.json',
|
|
1413
2109
|
hash: canonicalHash(buildRecord),
|
|
1414
2110
|
},
|
|
1415
|
-
warnings:
|
|
2111
|
+
warnings: retainedWarnings(state.warnings),
|
|
1416
2112
|
};
|
|
1417
2113
|
}
|
|
1418
2114
|
|
|
@@ -1469,8 +2165,20 @@ async function createAuthoredContent(state, options, now) {
|
|
|
1469
2165
|
errors: [],
|
|
1470
2166
|
};
|
|
1471
2167
|
const authored = [];
|
|
2168
|
+
const artifactLinkTables = new Map(
|
|
2169
|
+
artifacts.map((artifact) => [
|
|
2170
|
+
artifact.id,
|
|
2171
|
+
canonicalArtifactLinks(artifacts, artifact.id, state.run.slug),
|
|
2172
|
+
]),
|
|
2173
|
+
);
|
|
1472
2174
|
for (const artifact of artifacts) {
|
|
1473
|
-
const item = await authorArtifact(
|
|
2175
|
+
const item = await authorArtifact(
|
|
2176
|
+
state,
|
|
2177
|
+
artifact,
|
|
2178
|
+
author,
|
|
2179
|
+
trust,
|
|
2180
|
+
artifactLinkTables.get(artifact.id),
|
|
2181
|
+
);
|
|
1474
2182
|
if ((item.result.proposedArtifacts ?? []).length > 0) {
|
|
1475
2183
|
throw codedError(
|
|
1476
2184
|
'E_AUTHOR_RESULT',
|
|
@@ -1583,6 +2291,7 @@ async function authorArtifact(
|
|
|
1583
2291
|
artifact,
|
|
1584
2292
|
author,
|
|
1585
2293
|
trust,
|
|
2294
|
+
artifactLinks,
|
|
1586
2295
|
correctionContext,
|
|
1587
2296
|
) {
|
|
1588
2297
|
const [brief, visualAuthoringGuidance, shellContent] = await Promise.all([
|
|
@@ -1613,7 +2322,7 @@ async function authorArtifact(
|
|
|
1613
2322
|
);
|
|
1614
2323
|
}
|
|
1615
2324
|
const authorRequest = {
|
|
1616
|
-
schemaVersion: 'explainer-kit.author-request/
|
|
2325
|
+
schemaVersion: 'explainer-kit.author-request/v3',
|
|
1617
2326
|
artifactId: artifact.id,
|
|
1618
2327
|
artifactType: artifact.type,
|
|
1619
2328
|
authoring: artifact.authoring,
|
|
@@ -1624,6 +2333,7 @@ async function authorArtifact(
|
|
|
1624
2333
|
theme: structuredClone(state.theme),
|
|
1625
2334
|
setContext: structuredClone(state.setPlan),
|
|
1626
2335
|
plannedArtifact: structuredClone(artifact.plannedArtifact),
|
|
2336
|
+
artifactLinks: structuredClone(artifactLinks),
|
|
1627
2337
|
...(graphSemantics.length > 0 && {
|
|
1628
2338
|
graphSemantics: structuredClone(graphSemantics),
|
|
1629
2339
|
}),
|
|
@@ -1633,7 +2343,7 @@ async function authorArtifact(
|
|
|
1633
2343
|
}),
|
|
1634
2344
|
};
|
|
1635
2345
|
const requestValidation = validateContract(
|
|
1636
|
-
'author-request/
|
|
2346
|
+
'author-request/v3',
|
|
1637
2347
|
authorRequest,
|
|
1638
2348
|
);
|
|
1639
2349
|
if (!requestValidation.valid) {
|
|
@@ -1643,13 +2353,22 @@ async function authorArtifact(
|
|
|
1643
2353
|
);
|
|
1644
2354
|
}
|
|
1645
2355
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
2356
|
+
let result;
|
|
2357
|
+
try {
|
|
2358
|
+
result =
|
|
2359
|
+
correctionContext === undefined
|
|
2360
|
+
? await author(structuredClone(authorRequest))
|
|
2361
|
+
: await author(
|
|
2362
|
+
structuredClone(authorRequest),
|
|
2363
|
+
structuredClone(correctionContext),
|
|
2364
|
+
);
|
|
2365
|
+
} catch {
|
|
2366
|
+
throw withEvidenceReason(
|
|
2367
|
+
codedError('E_AUTHOR', 'Artifact author provider failed.'),
|
|
2368
|
+
'authoring',
|
|
2369
|
+
'provider-failure',
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
1653
2372
|
const resultValidation = validateContract('author-result/v2', result);
|
|
1654
2373
|
if (!resultValidation.valid) {
|
|
1655
2374
|
throw codedError(
|
|
@@ -1718,6 +2437,34 @@ async function authorArtifact(
|
|
|
1718
2437
|
};
|
|
1719
2438
|
}
|
|
1720
2439
|
|
|
2440
|
+
function canonicalArtifactLinks(artifacts, currentArtifactId, slug) {
|
|
2441
|
+
const paths = new Map(
|
|
2442
|
+
artifacts.map((artifact) => [
|
|
2443
|
+
artifact.id,
|
|
2444
|
+
artifactPath(renderDescriptor(artifact), slug),
|
|
2445
|
+
]),
|
|
2446
|
+
);
|
|
2447
|
+
const currentPath = paths.get(currentArtifactId);
|
|
2448
|
+
if (!currentPath) {
|
|
2449
|
+
throw codedError(
|
|
2450
|
+
'E_AUTHOR_REQUEST',
|
|
2451
|
+
`Cannot construct canonical links for unknown artifact ${currentArtifactId}.`,
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
return artifacts.map((artifact) => {
|
|
2455
|
+
const sitePath = paths.get(artifact.id);
|
|
2456
|
+
const href =
|
|
2457
|
+
posix.relative(posix.dirname(currentPath), sitePath) ||
|
|
2458
|
+
posix.basename(sitePath);
|
|
2459
|
+
return {
|
|
2460
|
+
artifactId: artifact.id,
|
|
2461
|
+
artifactType: artifact.type,
|
|
2462
|
+
sitePath,
|
|
2463
|
+
href,
|
|
2464
|
+
};
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
1721
2468
|
function diagramAnalyses(markdown) {
|
|
1722
2469
|
const ast = parseMarkdown(markdown);
|
|
1723
2470
|
const diagrams = [];
|
|
@@ -2004,6 +2751,7 @@ async function immutableHashesFor(state) {
|
|
|
2004
2751
|
metricsPath,
|
|
2005
2752
|
]),
|
|
2006
2753
|
...state.visualReviewPaths,
|
|
2754
|
+
...(state.publishReceiptPath ? [state.publishReceiptPath] : []),
|
|
2007
2755
|
...(state.theme ? ['theme.resolved.json'] : []),
|
|
2008
2756
|
...state.artifacts
|
|
2009
2757
|
.filter(
|
|
@@ -2050,13 +2798,22 @@ function assertValidRequest(request) {
|
|
|
2050
2798
|
}
|
|
2051
2799
|
}
|
|
2052
2800
|
|
|
2053
|
-
function
|
|
2801
|
+
function terminalReasonsForReview(state) {
|
|
2802
|
+
const visualReasons = state.visualReview?.evidence?.reasons ?? [];
|
|
2803
|
+
if (visualReasons.length > 0) return structuredClone(visualReasons);
|
|
2804
|
+
return [
|
|
2805
|
+
state.reviewGateReason ??
|
|
2806
|
+
evidenceReason('visual-review', 'pipeline-failure'),
|
|
2807
|
+
];
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
function resultFor(state, failure = { failed: false }) {
|
|
2054
2811
|
return {
|
|
2055
2812
|
runId: state.run.runId,
|
|
2056
2813
|
runRoot: state.run.runRoot,
|
|
2057
2814
|
manifestPath: state.run.manifestPath,
|
|
2058
2815
|
buildRecordPath: state.run.buildRecordPath,
|
|
2059
|
-
outcome:
|
|
2816
|
+
outcome: failure.failed
|
|
2060
2817
|
? 'failed'
|
|
2061
2818
|
: state.approval?.canResume === false
|
|
2062
2819
|
? 'incomplete'
|
|
@@ -2066,8 +2823,13 @@ function resultFor(state, error) {
|
|
|
2066
2823
|
...(state.approval?.record?.marking && {
|
|
2067
2824
|
marking: state.approval.record.marking,
|
|
2068
2825
|
}),
|
|
2069
|
-
warnings:
|
|
2070
|
-
discovery:
|
|
2826
|
+
warnings: retainedWarnings(state.warnings),
|
|
2827
|
+
discovery: {
|
|
2828
|
+
rounds: state.discovery.rounds,
|
|
2829
|
+
findingCount: state.discovery.findings.length,
|
|
2830
|
+
reason: state.discovery.reason,
|
|
2831
|
+
},
|
|
2832
|
+
...(state.publication && { publication: state.publication }),
|
|
2071
2833
|
...(state.approval && {
|
|
2072
2834
|
approval: {
|
|
2073
2835
|
status: state.approval.status,
|
|
@@ -2079,14 +2841,60 @@ function resultFor(state, error) {
|
|
|
2079
2841
|
},
|
|
2080
2842
|
}),
|
|
2081
2843
|
...(state.visualReview && {
|
|
2082
|
-
visualReview: structuredClone(state.visualReview.
|
|
2844
|
+
visualReview: structuredClone(state.visualReview.evidence),
|
|
2083
2845
|
}),
|
|
2084
|
-
...(
|
|
2085
|
-
|
|
2846
|
+
...(failure.failed && {
|
|
2847
|
+
reasons: structuredClone(failure.reasons),
|
|
2086
2848
|
}),
|
|
2087
2849
|
};
|
|
2088
2850
|
}
|
|
2089
2851
|
|
|
2852
|
+
function publicationSummary(receipt) {
|
|
2853
|
+
if (receipt.schemaVersion === 'explainer-kit.publish-receipt/v2') {
|
|
2854
|
+
return {
|
|
2855
|
+
schemaVersion: 'explainer-kit.publish-summary/v2',
|
|
2856
|
+
receiptSchemaVersion: receipt.schemaVersion,
|
|
2857
|
+
publicAccess: receipt.publicAccess,
|
|
2858
|
+
artifacts: receipt.artifacts.map((artifact) => structuredClone(artifact)),
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
return {
|
|
2862
|
+
schemaVersion: 'explainer-kit.publish-summary/v1',
|
|
2863
|
+
receiptSchemaVersion: receipt.schemaVersion,
|
|
2864
|
+
publicAccess: 'public',
|
|
2865
|
+
artifacts: receipt.artifacts.map(({ relativePath, publicUrl }) => ({
|
|
2866
|
+
relativePath,
|
|
2867
|
+
publicUrl,
|
|
2868
|
+
})),
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function publicationValidationContext(receipt, manifest) {
|
|
2873
|
+
if (receipt.schemaVersion !== 'explainer-kit.publish-receipt/v2') {
|
|
2874
|
+
return { manifest };
|
|
2875
|
+
}
|
|
2876
|
+
let catalog;
|
|
2877
|
+
try {
|
|
2878
|
+
// `publicAccess` is a receipt/v2 field; a v1 receipt returns above, and the
|
|
2879
|
+
// connector resolves v1 publish requests to 'public' for the same reason.
|
|
2880
|
+
catalog = catalogFromManifest(manifest, receipt.roots.publicBaseUrl, {
|
|
2881
|
+
publicAccess: receipt.publicAccess,
|
|
2882
|
+
});
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
throw codedError(
|
|
2885
|
+
'E_PUBLISH',
|
|
2886
|
+
`Publisher returned invalid publication roots: ${safeMessage(error)}`,
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
return {
|
|
2890
|
+
manifest,
|
|
2891
|
+
catalogArtifact: {
|
|
2892
|
+
relativePath: initiativeCatalogPath(manifest.slug),
|
|
2893
|
+
hash: hashBytes(Buffer.from(serializeInitiativeCatalog(catalog))),
|
|
2894
|
+
},
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2090
2898
|
async function parseCli(argv) {
|
|
2091
2899
|
let requestPath;
|
|
2092
2900
|
const options = {};
|
|
@@ -2155,17 +2963,71 @@ function codedError(code, message) {
|
|
|
2155
2963
|
return error;
|
|
2156
2964
|
}
|
|
2157
2965
|
|
|
2966
|
+
/**
|
|
2967
|
+
* Terminal evidence reasons are unique by (stage, kind), so a second reason for
|
|
2968
|
+
* a pair that is already present folds into its count rather than appending a
|
|
2969
|
+
* duplicate the closed contract would reject.
|
|
2970
|
+
*/
|
|
2971
|
+
function mergeTerminalReasons(reasons, addition) {
|
|
2972
|
+
const existing = reasons.find(
|
|
2973
|
+
({ stage, kind }) => stage === addition.stage && kind === addition.kind,
|
|
2974
|
+
);
|
|
2975
|
+
if (!existing) return [...reasons, addition];
|
|
2976
|
+
return reasons.map((entry) =>
|
|
2977
|
+
entry === existing
|
|
2978
|
+
? { ...entry, count: (entry.count ?? 1) + (addition.count ?? 1) }
|
|
2979
|
+
: entry,
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function withEvidenceReason(error, stage, kind) {
|
|
2984
|
+
error.evidenceReason = evidenceReason(stage, kind);
|
|
2985
|
+
LOCALLY_PROJECTED_ERRORS.add(error);
|
|
2986
|
+
return error;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
function localEvidenceReason(error) {
|
|
2990
|
+
return error !== null &&
|
|
2991
|
+
(typeof error === 'object' || typeof error === 'function') &&
|
|
2992
|
+
LOCALLY_PROJECTED_ERRORS.has(error)
|
|
2993
|
+
? error.evidenceReason
|
|
2994
|
+
: null;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
function evidenceStageForBuildStage(stage) {
|
|
2998
|
+
return (
|
|
2999
|
+
{
|
|
3000
|
+
validate: 'planning',
|
|
3001
|
+
'fact-base': 'planning',
|
|
3002
|
+
content: 'authoring',
|
|
3003
|
+
theme: 'authoring',
|
|
3004
|
+
render: 'rendering',
|
|
3005
|
+
qa: 'browser-review',
|
|
3006
|
+
durability: 'durability',
|
|
3007
|
+
publish: 'durability',
|
|
3008
|
+
}[stage] ?? 'finalization'
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2158
3012
|
function stageErrorCode(stage) {
|
|
2159
3013
|
return `E_${stage.toUpperCase().replaceAll('-', '_')}`;
|
|
2160
3014
|
}
|
|
2161
3015
|
|
|
2162
|
-
function safeMessage(
|
|
2163
|
-
return
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
3016
|
+
function safeMessage(_error) {
|
|
3017
|
+
return 'Operation failed.';
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
function retainedWarnings(warnings) {
|
|
3021
|
+
return [...new Set(warnings.filter(isLocalWarningCode))];
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function isLocalWarningCode(value) {
|
|
3025
|
+
return (
|
|
3026
|
+
typeof value === 'string' &&
|
|
3027
|
+
/^(?:fact-base-freshness-warning|theme-selection-normalized|durability-evidence-required|publish-receipt-evidence-required|(?:expansion|guideline|render|qa)-[a-z0-9-]+|visual-review-required:[a-z0-9-]+|stage-reopened:[a-z0-9-]+:[0-9TZ:.-]+|missing-(?:theme-token|required-anchor):[a-z0-9-]+)$/.test(
|
|
3028
|
+
value,
|
|
2167
3029
|
)
|
|
2168
|
-
|
|
3030
|
+
);
|
|
2169
3031
|
}
|
|
2170
3032
|
|
|
2171
3033
|
if (
|