@open-agent-toolkit/cli 0.2.73 → 0.2.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { validateContract } from './contracts.mjs';
|
|
6
|
+
import {
|
|
7
|
+
enforceRunPackageInventory,
|
|
8
|
+
validateImmutablePackageEvidence,
|
|
9
|
+
} from './package-coverage.mjs';
|
|
10
|
+
import { validateQaResult } from './qa-result.mjs';
|
|
11
|
+
|
|
12
|
+
const SATISFIED_OUTCOMES = new Set(['built', 'built-needs-review']);
|
|
13
|
+
const SCREENSHOT_PATHS = ['qa/320.png', 'qa/768.png', 'qa/1440.png'];
|
|
14
|
+
|
|
15
|
+
export async function validateSatisfiedRunPackage(
|
|
16
|
+
runRoot,
|
|
17
|
+
recipe,
|
|
18
|
+
{ inputHashes } = {},
|
|
19
|
+
) {
|
|
20
|
+
const manifest = JSON.parse(
|
|
21
|
+
await readFile(join(runRoot, 'manifest.json'), 'utf8'),
|
|
22
|
+
);
|
|
23
|
+
const validation = validateContract('manifest', manifest);
|
|
24
|
+
if (!validation.valid) {
|
|
25
|
+
throw packageError('manifest-v2-contract');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const recipeId = typeof recipe === 'string' ? recipe : recipe.id;
|
|
29
|
+
const recipeVersion = typeof recipe === 'string' ? undefined : recipe.version;
|
|
30
|
+
const artifact = manifest.artifacts[0];
|
|
31
|
+
const expectedArtifact = typeof recipe === 'string' ? null : recipe.floor[0];
|
|
32
|
+
if (
|
|
33
|
+
manifest.recipe.id !== recipeId ||
|
|
34
|
+
(recipeVersion !== undefined &&
|
|
35
|
+
manifest.recipe.version !== recipeVersion) ||
|
|
36
|
+
!SATISFIED_OUTCOMES.has(manifest.outcome) ||
|
|
37
|
+
artifact?.id !== expectedArtifact?.id ||
|
|
38
|
+
artifact?.type !== expectedArtifact?.type ||
|
|
39
|
+
artifact?.contentPath !== 'site/index.html' ||
|
|
40
|
+
artifact?.status !== 'built' ||
|
|
41
|
+
manifest.artifacts.length !== 1
|
|
42
|
+
) {
|
|
43
|
+
throw packageError('project-recap-identity-or-outcome');
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
inputHashes !== undefined &&
|
|
47
|
+
!equalInputHashes(manifest.source.inputHashes, inputHashes)
|
|
48
|
+
) {
|
|
49
|
+
throw packageError('input-hashes');
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
manifest.source.factBasePath !== 'source/fact-base.json' ||
|
|
53
|
+
manifest.source.factBaseHash !==
|
|
54
|
+
manifest.immutableHashes[manifest.source.factBasePath] ||
|
|
55
|
+
manifest.theme.path !== 'theme.resolved.json' ||
|
|
56
|
+
manifest.theme.hash !== manifest.immutableHashes[manifest.theme.path] ||
|
|
57
|
+
artifact.hash !== manifest.immutableHashes[artifact.contentPath]
|
|
58
|
+
) {
|
|
59
|
+
throw packageError('canonical-path-hash-bindings');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
validateImmutablePackageEvidence(manifest);
|
|
63
|
+
await verifyImmutableBytes(runRoot, manifest.immutableHashes);
|
|
64
|
+
await enforceRunPackageInventory(runRoot, manifest);
|
|
65
|
+
|
|
66
|
+
const qa = validateQaResult(
|
|
67
|
+
JSON.parse(await readFile(join(runRoot, 'qa/result.json'), 'utf8')),
|
|
68
|
+
);
|
|
69
|
+
const expectedScreenshots = SCREENSHOT_PATHS.filter(
|
|
70
|
+
(path) => path in manifest.immutableHashes,
|
|
71
|
+
);
|
|
72
|
+
if (
|
|
73
|
+
qa.artifactSha256 !== artifact.hash ||
|
|
74
|
+
Object.values(qa.checks).some(({ status }) => status !== 'pass') ||
|
|
75
|
+
outcomeFromQa(qa) !== manifest.outcome ||
|
|
76
|
+
JSON.stringify(qa.screenshots ?? []) !== JSON.stringify(expectedScreenshots)
|
|
77
|
+
) {
|
|
78
|
+
throw packageError('qa-artifact-outcome-bindings');
|
|
79
|
+
}
|
|
80
|
+
return manifest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function equalInputHashes(left, right) {
|
|
84
|
+
const leftEntries = Object.entries(left).sort(([a], [b]) =>
|
|
85
|
+
a.localeCompare(b),
|
|
86
|
+
);
|
|
87
|
+
const rightEntries = Object.entries(right).sort(([a], [b]) =>
|
|
88
|
+
a.localeCompare(b),
|
|
89
|
+
);
|
|
90
|
+
return (
|
|
91
|
+
leftEntries.length === rightEntries.length &&
|
|
92
|
+
leftEntries.every(
|
|
93
|
+
([key, value], index) =>
|
|
94
|
+
key === rightEntries[index][0] && value === rightEntries[index][1],
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function verifyImmutableBytes(runRoot, immutableHashes) {
|
|
100
|
+
for (const [path, expected] of Object.entries(immutableHashes)) {
|
|
101
|
+
const actual = `sha256:${createHash('sha256')
|
|
102
|
+
.update(await readFile(join(runRoot, path)))
|
|
103
|
+
.digest('hex')}`;
|
|
104
|
+
if (actual !== expected) {
|
|
105
|
+
throw packageError(`immutable-byte-hash:${path}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function outcomeFromQa(qa) {
|
|
111
|
+
return qa.rung !== 'none' && qa.visual.verdict === 'pass'
|
|
112
|
+
? 'built'
|
|
113
|
+
: 'built-needs-review';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function packageError(reason) {
|
|
117
|
+
const error = new Error(`Satisfied run package is invalid: ${reason}.`);
|
|
118
|
+
error.code = 'E_RUN_PACKAGE';
|
|
119
|
+
return error;
|
|
120
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const FILE_URL_ABSOLUTE_PATH = /(^|[\s"'([{=])file:\/\/[^\s"'()[\]{}<>;,]*/gi;
|
|
2
|
+
const POSIX_ABSOLUTE_PATH = /(^|[\s"'([{=])\/[^\s"'()[\]{}<>;,:]*/g;
|
|
3
|
+
const WINDOWS_DRIVE_ABSOLUTE_PATH =
|
|
4
|
+
/(^|[\s"'([{=])[A-Za-z]:[\\/][^\s"'()[\]{}<>;,:]*/g;
|
|
5
|
+
const WINDOWS_UNC_ABSOLUTE_PATH =
|
|
6
|
+
/(^|[\s"'([{=])\\\\[^\\/\s"'()[\]{}<>;,:]+[\\/][^\s"'()[\]{}<>;,:]*/g;
|
|
7
|
+
const SENSITIVE_ENVIRONMENT_NAME =
|
|
8
|
+
/(?:^|_)(?:ACCESS_?KEY|API_?KEY|AUTH|BEARER|CREDENTIALS?|PASSWORD|PASSWD|PAT|PRIVATE_?KEY|SECRET_?KEY|SECRET|SESSION|TOKEN)$/i;
|
|
9
|
+
const NON_SECRET_STRUCTURAL_ENVIRONMENT_NAMES = new Set(['npm_package_name']);
|
|
10
|
+
const DISTINCTIVE_ENVIRONMENT_VALUE_LENGTH = 12;
|
|
11
|
+
const SENSITIVE_ENVIRONMENT_VALUE_LENGTH = 4;
|
|
12
|
+
|
|
13
|
+
function environmentValuesForRedaction(env) {
|
|
14
|
+
return [
|
|
15
|
+
...new Set(
|
|
16
|
+
Object.entries(env)
|
|
17
|
+
.filter(
|
|
18
|
+
([name, entry]) =>
|
|
19
|
+
typeof entry === 'string' &&
|
|
20
|
+
((entry.length >= SENSITIVE_ENVIRONMENT_VALUE_LENGTH &&
|
|
21
|
+
SENSITIVE_ENVIRONMENT_NAME.test(name)) ||
|
|
22
|
+
(entry.length >= DISTINCTIVE_ENVIRONMENT_VALUE_LENGTH &&
|
|
23
|
+
!NON_SECRET_STRUCTURAL_ENVIRONMENT_NAMES.has(name))),
|
|
24
|
+
)
|
|
25
|
+
.map(([_name, entry]) => entry),
|
|
26
|
+
),
|
|
27
|
+
].sort(
|
|
28
|
+
(left, right) => right.length - left.length || left.localeCompare(right),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeDiagnostic(value, { env = process.env } = {}) {
|
|
33
|
+
let sanitized = value instanceof Error ? value.message : String(value);
|
|
34
|
+
for (const environmentValue of environmentValuesForRedaction(env)) {
|
|
35
|
+
sanitized = sanitized.replaceAll(environmentValue, '<env>');
|
|
36
|
+
}
|
|
37
|
+
return sanitized
|
|
38
|
+
.replace(FILE_URL_ABSOLUTE_PATH, (_match, prefix) => `${prefix}<path>`)
|
|
39
|
+
.replace(WINDOWS_UNC_ABSOLUTE_PATH, (_match, prefix) => `${prefix}<path>`)
|
|
40
|
+
.replace(WINDOWS_DRIVE_ABSOLUTE_PATH, (_match, prefix) => `${prefix}<path>`)
|
|
41
|
+
.replace(POSIX_ABSOLUTE_PATH, (_match, prefix) => `${prefix}<path>`);
|
|
42
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { access, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { validateContract } from './lib/contracts.mjs';
|
|
9
|
+
import {
|
|
10
|
+
permissibleRunPackagePaths,
|
|
11
|
+
requiredImmutablePackagePaths,
|
|
12
|
+
validateImmutablePackageEvidence,
|
|
13
|
+
} from './lib/package-coverage.mjs';
|
|
14
|
+
import { validateQaResult } from './lib/qa-result.mjs';
|
|
15
|
+
import { loadRecipe } from './lib/recipes.mjs';
|
|
16
|
+
import { sanitizeDiagnostic } from './lib/sanitize.mjs';
|
|
17
|
+
|
|
18
|
+
const HASH_PREFIX = 'sha256:';
|
|
19
|
+
const MODES = new Set(['unattended', 'interactive']);
|
|
20
|
+
const BROWSER_RUNGS = new Set(['host', 'playwright']);
|
|
21
|
+
|
|
22
|
+
export async function recordRun({
|
|
23
|
+
runRoot,
|
|
24
|
+
recipe,
|
|
25
|
+
slug,
|
|
26
|
+
mode,
|
|
27
|
+
themePath,
|
|
28
|
+
runId = randomUUID(),
|
|
29
|
+
createdAt = new Date().toISOString(),
|
|
30
|
+
}) {
|
|
31
|
+
if (!runRoot || !recipe || !slug || !mode || !themePath) {
|
|
32
|
+
throw recordError(
|
|
33
|
+
'record-invalid-arguments',
|
|
34
|
+
'Record arguments are incomplete.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (!MODES.has(mode)) {
|
|
38
|
+
throw recordError(
|
|
39
|
+
'record-invalid-mode',
|
|
40
|
+
`Unsupported record mode: ${mode}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (await exists(join(runRoot, 'failure.json'))) {
|
|
44
|
+
throw recordError(
|
|
45
|
+
'record-failure-present',
|
|
46
|
+
'Cannot record a run that contains failure.json.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const recipeVersion = recipe === 'project-recap' ? '2' : '1';
|
|
51
|
+
const recipeContract = loadRecipe(recipe, recipeVersion);
|
|
52
|
+
const contentPath = 'site/index.html';
|
|
53
|
+
const factBasePath = 'source/fact-base.json';
|
|
54
|
+
const resolvedThemePath = 'theme.resolved.json';
|
|
55
|
+
const [factBaseBytes, themeBytes, artifactBytes] = await Promise.all([
|
|
56
|
+
readFile(join(runRoot, factBasePath)),
|
|
57
|
+
readFile(themePath),
|
|
58
|
+
readFile(join(runRoot, contentPath)),
|
|
59
|
+
]);
|
|
60
|
+
const artifactHash = hashBytes(artifactBytes);
|
|
61
|
+
|
|
62
|
+
let qa = null;
|
|
63
|
+
try {
|
|
64
|
+
qa = JSON.parse(await readFile(join(runRoot, 'qa/result.json'), 'utf8'));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== 'ENOENT') throw error;
|
|
67
|
+
}
|
|
68
|
+
if (qa) {
|
|
69
|
+
try {
|
|
70
|
+
validateQaResult(qa);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw recordError(
|
|
73
|
+
'record-qa-invalid',
|
|
74
|
+
`qa/result.json is invalid: ${error.message}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (qa && qa.artifactSha256 !== artifactHash) {
|
|
79
|
+
throw recordError(
|
|
80
|
+
'record-qa-stale',
|
|
81
|
+
'qa/result.json describes different site/index.html bytes.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { outcome, warnings } = outcomeFromQa(qa);
|
|
86
|
+
const immutableHashes = await hashRunFiles(runRoot);
|
|
87
|
+
const screenshotPaths = ['qa/320.png', 'qa/768.png', 'qa/1440.png'].filter(
|
|
88
|
+
(path) => path in immutableHashes,
|
|
89
|
+
);
|
|
90
|
+
if (
|
|
91
|
+
JSON.stringify(screenshotPaths) !== JSON.stringify(qa?.screenshots ?? [])
|
|
92
|
+
) {
|
|
93
|
+
throw recordError(
|
|
94
|
+
'record-qa-invalid',
|
|
95
|
+
'QA screenshot claims do not match the canonical screenshot files.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const factBase = JSON.parse(factBaseBytes.toString('utf8'));
|
|
99
|
+
const bundleInputs = (factBase.sources ?? []).filter(
|
|
100
|
+
({ role }) => role === 'bundle-input',
|
|
101
|
+
);
|
|
102
|
+
const manifestInputs =
|
|
103
|
+
bundleInputs.length > 0 ? bundleInputs : (factBase.sources ?? []);
|
|
104
|
+
const manifest = {
|
|
105
|
+
schemaVersion: 'explainer-kit.manifest/v2',
|
|
106
|
+
runId,
|
|
107
|
+
slug,
|
|
108
|
+
recipe: { id: recipeContract.id, version: recipeContract.version },
|
|
109
|
+
createdAt,
|
|
110
|
+
mode,
|
|
111
|
+
source: {
|
|
112
|
+
factBasePath,
|
|
113
|
+
factBaseHash: hashBytes(factBaseBytes),
|
|
114
|
+
inputHashes: Object.fromEntries(
|
|
115
|
+
manifestInputs
|
|
116
|
+
.filter(
|
|
117
|
+
({ locator, hash }) =>
|
|
118
|
+
typeof locator === 'string' && typeof hash === 'string',
|
|
119
|
+
)
|
|
120
|
+
.map(({ locator, hash }) => [locator, hash])
|
|
121
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
theme: {
|
|
125
|
+
path: resolvedThemePath,
|
|
126
|
+
hash: hashBytes(themeBytes),
|
|
127
|
+
},
|
|
128
|
+
artifacts: [
|
|
129
|
+
{
|
|
130
|
+
id: recipeContract.floor[0].id,
|
|
131
|
+
type: recipeContract.floor[0].type,
|
|
132
|
+
contentPath,
|
|
133
|
+
hash: artifactHash,
|
|
134
|
+
status:
|
|
135
|
+
outcome === 'failed' || outcome === 'incomplete' ? 'failed' : 'built',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
immutableHashes,
|
|
139
|
+
outcome,
|
|
140
|
+
warnings,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const validation = validateContract('manifest', manifest);
|
|
144
|
+
if (!validation.valid) {
|
|
145
|
+
throw recordError(
|
|
146
|
+
'record-manifest-invalid',
|
|
147
|
+
`Generated manifest is invalid: ${validation.errors
|
|
148
|
+
.map(({ code }) => code)
|
|
149
|
+
.join(', ')}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const permissible = new Set(
|
|
153
|
+
permissibleRunPackagePaths(manifest).filter(
|
|
154
|
+
(path) => path !== 'manifest.json',
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
const unexpected = Object.keys(immutableHashes).filter(
|
|
158
|
+
(path) => !permissible.has(path),
|
|
159
|
+
);
|
|
160
|
+
if (unexpected.length > 0) {
|
|
161
|
+
throw recordError(
|
|
162
|
+
'record-package-unexpected',
|
|
163
|
+
`Run package contains unexpected files: ${unexpected.join(', ')}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (qa) {
|
|
167
|
+
try {
|
|
168
|
+
validateImmutablePackageEvidence(manifest);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw recordError('record-package-incomplete', error.message);
|
|
171
|
+
}
|
|
172
|
+
const expected = requiredImmutablePackagePaths(manifest).sort();
|
|
173
|
+
const missing = expected.filter((path) => !(path in immutableHashes));
|
|
174
|
+
if (missing.length > 0) {
|
|
175
|
+
throw recordError(
|
|
176
|
+
'record-package-incomplete',
|
|
177
|
+
`Run package is incomplete: ${missing.join(', ')}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const temporaryManifest = join(
|
|
183
|
+
runRoot,
|
|
184
|
+
`.manifest-${process.pid}-${randomUUID()}.json`,
|
|
185
|
+
);
|
|
186
|
+
await writeFile(temporaryManifest, formatManifest(manifest));
|
|
187
|
+
await rename(temporaryManifest, join(runRoot, 'manifest.json'));
|
|
188
|
+
return manifest;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function runRecord(argv, io = console) {
|
|
192
|
+
const options = parseArgs(argv);
|
|
193
|
+
const manifest = await recordRun(options);
|
|
194
|
+
io.log(
|
|
195
|
+
JSON.stringify({ runRoot: options.runRoot, outcome: manifest.outcome }),
|
|
196
|
+
);
|
|
197
|
+
return manifest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function outcomeFromQa(qa) {
|
|
201
|
+
if (!qa) {
|
|
202
|
+
return {
|
|
203
|
+
outcome: 'incomplete',
|
|
204
|
+
warnings: ['record-incomplete:qa-missing'],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const checks = Object.entries(qa.checks).map(([id, check]) => ({
|
|
208
|
+
id,
|
|
209
|
+
...check,
|
|
210
|
+
}));
|
|
211
|
+
const failures = checks.filter(({ status }) => status !== 'pass');
|
|
212
|
+
if (failures.length > 0) {
|
|
213
|
+
const failureSummary = failures
|
|
214
|
+
.map(({ id, cause, message }) => `${id}: ${cause ?? message ?? 'failed'}`)
|
|
215
|
+
.join('; ');
|
|
216
|
+
return {
|
|
217
|
+
outcome: 'failed',
|
|
218
|
+
warnings: [`record-failed:${sanitizeDiagnostic(failureSummary)}`],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (BROWSER_RUNGS.has(qa.rung) && qa.visual?.verdict === 'pass') {
|
|
222
|
+
return { outcome: 'built', warnings: [] };
|
|
223
|
+
}
|
|
224
|
+
const reason =
|
|
225
|
+
qa.reason ??
|
|
226
|
+
(qa.visual?.verdict === 'findings'
|
|
227
|
+
? (qa.visual.findings ?? ['visual-findings']).join(',')
|
|
228
|
+
: 'visual-review-unavailable');
|
|
229
|
+
return {
|
|
230
|
+
outcome: 'built-needs-review',
|
|
231
|
+
warnings: [`record-needs-review:${sanitizeDiagnostic(reason)}`],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function hashRunFiles(runRoot) {
|
|
236
|
+
const hashes = {};
|
|
237
|
+
async function visit(relativeRoot = '') {
|
|
238
|
+
const entries = await readdir(join(runRoot, relativeRoot), {
|
|
239
|
+
withFileTypes: true,
|
|
240
|
+
});
|
|
241
|
+
for (const entry of entries.sort((left, right) =>
|
|
242
|
+
left.name.localeCompare(right.name),
|
|
243
|
+
)) {
|
|
244
|
+
const path = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
|
|
245
|
+
if (path === 'manifest.json' || path.startsWith('.manifest-')) continue;
|
|
246
|
+
if (entry.isSymbolicLink()) {
|
|
247
|
+
throw recordError(
|
|
248
|
+
'record-package-symlink',
|
|
249
|
+
`Run package contains a symbolic link: ${path}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
await visit(path);
|
|
254
|
+
} else if (entry.isFile()) {
|
|
255
|
+
hashes[path] = hashBytes(await readFile(join(runRoot, path)));
|
|
256
|
+
} else {
|
|
257
|
+
throw recordError(
|
|
258
|
+
'record-package-entry',
|
|
259
|
+
`Run package contains an unsupported entry: ${path}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await visit();
|
|
265
|
+
return hashes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseArgs(argv) {
|
|
269
|
+
const options = {};
|
|
270
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
271
|
+
const field = {
|
|
272
|
+
'--run-root': 'runRoot',
|
|
273
|
+
'--recipe': 'recipe',
|
|
274
|
+
'--slug': 'slug',
|
|
275
|
+
'--mode': 'mode',
|
|
276
|
+
'--theme': 'themePath',
|
|
277
|
+
'--run-id': 'runId',
|
|
278
|
+
'--created-at': 'createdAt',
|
|
279
|
+
}[argv[index]];
|
|
280
|
+
if (!field || !argv[index + 1]) throw usageError();
|
|
281
|
+
options[field] = argv[index + 1];
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
!options.runRoot ||
|
|
285
|
+
!options.recipe ||
|
|
286
|
+
!options.slug ||
|
|
287
|
+
!options.mode ||
|
|
288
|
+
!options.themePath
|
|
289
|
+
) {
|
|
290
|
+
throw usageError();
|
|
291
|
+
}
|
|
292
|
+
return options;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function hashBytes(bytes) {
|
|
296
|
+
return `${HASH_PREFIX}${createHash('sha256').update(bytes).digest('hex')}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatManifest(manifest) {
|
|
300
|
+
const json = JSON.stringify(manifest, null, 2).replace(
|
|
301
|
+
/ "warnings": \[\n(?: .+\n)* \]\n}$/,
|
|
302
|
+
` "warnings": ${JSON.stringify(manifest.warnings)}\n}`,
|
|
303
|
+
);
|
|
304
|
+
return `${json}\n`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function exists(path) {
|
|
308
|
+
try {
|
|
309
|
+
await access(path);
|
|
310
|
+
return true;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error.code === 'ENOENT') return false;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function usageError() {
|
|
318
|
+
return recordError(
|
|
319
|
+
'record-usage',
|
|
320
|
+
'Usage: record.mjs --run-root <dir> --recipe <id> --slug <slug> --mode unattended|interactive --theme <resolved-json> [--run-id <id>] [--created-at <iso>]',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function recordError(code, message) {
|
|
325
|
+
const error = new Error(message);
|
|
326
|
+
error.code = code;
|
|
327
|
+
return error;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
331
|
+
runRecord(process.argv.slice(2)).catch((error) => {
|
|
332
|
+
console.error(`${error.code}: ${error.message}`);
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
});
|
|
335
|
+
}
|