@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,13 @@
|
|
|
1
|
+
export const FLOW_FAILURE_STAGES = Object.freeze([
|
|
2
|
+
'bundle',
|
|
3
|
+
'authoring',
|
|
4
|
+
'verify',
|
|
5
|
+
'core',
|
|
6
|
+
'interrupted',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const FLOW_FAILURE_STAGE_SET = new Set(FLOW_FAILURE_STAGES);
|
|
10
|
+
|
|
11
|
+
export function isFlowFailureStage(value) {
|
|
12
|
+
return FLOW_FAILURE_STAGE_SET.has(value);
|
|
13
|
+
}
|
|
@@ -1,51 +1,75 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { lstat, readdir, realpath, rm } from 'node:fs/promises';
|
|
3
2
|
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export const PACKAGE_COVERAGE_VERSION = 'explainer-kit.package-coverage/v3';
|
|
5
|
+
const CONDITIONAL_SCREENSHOT_PATHS = [
|
|
6
|
+
'qa/320.png',
|
|
7
|
+
'qa/768.png',
|
|
8
|
+
'qa/1440.png',
|
|
9
|
+
];
|
|
7
10
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
export function requiredImmutablePackagePaths(manifest) {
|
|
12
|
+
if (!isObject(manifest)) {
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
'Manifest package coverage requires a manifest object.',
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return [
|
|
18
|
+
...new Set([
|
|
19
|
+
'theme.resolved.json',
|
|
20
|
+
'source/fact-base.json',
|
|
21
|
+
'source/fact-base.md',
|
|
22
|
+
'source/ledger.json',
|
|
23
|
+
'qa/result.json',
|
|
24
|
+
...(manifest.artifacts ?? []).map(({ contentPath }) => contentPath),
|
|
25
|
+
]),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
21
28
|
|
|
22
|
-
export function permissibleRunPackagePaths(
|
|
23
|
-
manifest,
|
|
24
|
-
{ includeManifest = true, includeTerminalEvidence = false } = {},
|
|
25
|
-
) {
|
|
29
|
+
export function permissibleRunPackagePaths(manifest) {
|
|
26
30
|
if (!isObject(manifest) || !isObject(manifest.immutableHashes)) {
|
|
27
31
|
throw new TypeError(
|
|
28
32
|
'Run package inventory requires a manifest with immutable hashes.',
|
|
29
33
|
);
|
|
30
34
|
}
|
|
31
35
|
const paths = new Set([
|
|
32
|
-
...
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
...requiredImmutablePackagePaths(manifest),
|
|
37
|
+
...CONDITIONAL_SCREENSHOT_PATHS.filter(
|
|
38
|
+
(path) => path in manifest.immutableHashes,
|
|
39
|
+
),
|
|
40
|
+
'manifest.json',
|
|
36
41
|
]);
|
|
37
42
|
for (const path of paths) assertInventoryPath(path);
|
|
38
43
|
return [...paths].sort();
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
export function validateImmutablePackageEvidence(manifest) {
|
|
47
|
+
if (!isObject(manifest) || !isObject(manifest.immutableHashes)) {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
'Manifest package coverage requires immutable hash evidence.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const expected = new Set(
|
|
53
|
+
permissibleRunPackagePaths(manifest).filter(
|
|
54
|
+
(path) => path !== 'manifest.json',
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
const actual = Object.keys(manifest.immutableHashes);
|
|
58
|
+
const missing = [...expected].filter(
|
|
59
|
+
(path) => !(path in manifest.immutableHashes),
|
|
60
|
+
);
|
|
61
|
+
const unexpected = actual.filter((path) => !expected.has(path));
|
|
62
|
+
if (missing.length > 0 || unexpected.length > 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Manifest immutable hashes do not match the canonical package: missing [${missing.join(', ')}]; unexpected [${unexpected.join(', ')}].`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
export async function enforceRunPackageInventory(
|
|
42
70
|
runRoot,
|
|
43
71
|
manifest,
|
|
44
|
-
{
|
|
45
|
-
includeManifest = true,
|
|
46
|
-
includeTerminalEvidence = false,
|
|
47
|
-
removeUnexpected = false,
|
|
48
|
-
} = {},
|
|
72
|
+
{ removeUnexpected = false } = {},
|
|
49
73
|
) {
|
|
50
74
|
if (typeof runRoot !== 'string' || runRoot.length === 0) {
|
|
51
75
|
throw new TypeError('Run package inventory requires a run root.');
|
|
@@ -56,12 +80,8 @@ export async function enforceRunPackageInventory(
|
|
|
56
80
|
throw new Error('Run package inventory root is not a real directory.');
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
includeManifest,
|
|
62
|
-
includeTerminalEvidence,
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
83
|
+
validateImmutablePackageEvidence(manifest);
|
|
84
|
+
const allowedFiles = new Set(permissibleRunPackagePaths(manifest));
|
|
65
85
|
const allowedDirectories = new Set();
|
|
66
86
|
for (const path of allowedFiles) {
|
|
67
87
|
let parent = dirname(path);
|
|
@@ -107,326 +127,6 @@ export async function enforceRunPackageInventory(
|
|
|
107
127
|
return [...presentFiles].sort();
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
export function requiredImmutablePackagePaths(manifest, { runMode } = {}) {
|
|
111
|
-
if (!isObject(manifest)) {
|
|
112
|
-
throw new TypeError(
|
|
113
|
-
'Manifest package coverage requires a manifest object.',
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (
|
|
117
|
-
runMode !== undefined &&
|
|
118
|
-
runMode !== 'interactive' &&
|
|
119
|
-
runMode !== 'unattended'
|
|
120
|
-
) {
|
|
121
|
-
throw new TypeError(
|
|
122
|
-
'Manifest package coverage run mode must be interactive or unattended.',
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const required = new Set([
|
|
127
|
-
'run-request.json',
|
|
128
|
-
'source/content-approval.json',
|
|
129
|
-
manifest.source?.factBasePath,
|
|
130
|
-
'source/fact-base.md',
|
|
131
|
-
...(manifest.source?.authorResultPaths ?? []),
|
|
132
|
-
manifest.theme?.path,
|
|
133
|
-
...(manifest.artifacts ?? []).flatMap((artifact) => [
|
|
134
|
-
artifact.contentPath,
|
|
135
|
-
...(artifact.status === 'built' &&
|
|
136
|
-
typeof artifact.renderedPath === 'string'
|
|
137
|
-
? [artifact.renderedPath]
|
|
138
|
-
: []),
|
|
139
|
-
]),
|
|
140
|
-
]);
|
|
141
|
-
required.delete(undefined);
|
|
142
|
-
|
|
143
|
-
if (manifest.recipe?.id !== 'project-recap') return [...required];
|
|
144
|
-
|
|
145
|
-
const successful = SUCCESSFUL_OUTCOMES.has(manifest.outcome);
|
|
146
|
-
if (successful) {
|
|
147
|
-
for (const path of SET_PLAN_RECORD_PATHS) required.add(path);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const recorded = Object.keys(manifest.immutableHashes ?? {});
|
|
151
|
-
if (manifest.outcome === PARTIAL_REVIEW_OUTCOME) return [...required];
|
|
152
|
-
|
|
153
|
-
const retainsReviewMaterial = recorded.some(isReviewMaterial);
|
|
154
|
-
const requireAttemptOne =
|
|
155
|
-
(successful && runMode === 'unattended') ||
|
|
156
|
-
(successful && runMode === 'interactive' && retainsReviewMaterial) ||
|
|
157
|
-
(!successful && retainsReviewMaterial);
|
|
158
|
-
const retainsAttemptTwo = recorded.some(isAttemptTwoMaterial);
|
|
159
|
-
|
|
160
|
-
if (requireAttemptOne || retainsAttemptTwo) {
|
|
161
|
-
addVisualReviewAttemptPaths(required, manifest, 1);
|
|
162
|
-
}
|
|
163
|
-
if (retainsAttemptTwo) {
|
|
164
|
-
required.add(VISUAL_REVISION_PATH);
|
|
165
|
-
addVisualReviewAttemptPaths(required, manifest, 2);
|
|
166
|
-
}
|
|
167
|
-
return [...required];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export async function validateImmutablePackageEvidence(
|
|
171
|
-
manifest,
|
|
172
|
-
{ runMode, read } = {},
|
|
173
|
-
) {
|
|
174
|
-
const requiredPaths = requiredImmutablePackagePaths(manifest, { runMode });
|
|
175
|
-
const missing = requiredPaths.filter(
|
|
176
|
-
(path) => !(path in (manifest?.immutableHashes ?? {})),
|
|
177
|
-
);
|
|
178
|
-
if (missing.length > 0) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`Manifest immutable hashes do not cover the canonical package: ${missing.join(', ')}.`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (manifest?.recipe?.id !== 'project-recap') return;
|
|
184
|
-
|
|
185
|
-
const recorded = Object.keys(manifest.immutableHashes ?? {});
|
|
186
|
-
if (recorded.some(isLegacyReviewMaterial)) {
|
|
187
|
-
throw new Error(
|
|
188
|
-
'Legacy review-gate evidence is not part of the canonical package.',
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
const successful = SUCCESSFUL_OUTCOMES.has(manifest.outcome);
|
|
192
|
-
const retainsReviewMaterial = recorded.some(isReviewMaterial);
|
|
193
|
-
if (manifest.outcome === PARTIAL_REVIEW_OUTCOME) return;
|
|
194
|
-
const requireAttemptOne =
|
|
195
|
-
(successful && runMode === 'unattended') ||
|
|
196
|
-
(successful && runMode === 'interactive' && retainsReviewMaterial) ||
|
|
197
|
-
(!successful &&
|
|
198
|
-
manifest.outcome !== PARTIAL_REVIEW_OUTCOME &&
|
|
199
|
-
retainsReviewMaterial);
|
|
200
|
-
const retainsAttemptTwo = recorded.some(isAttemptTwoMaterial);
|
|
201
|
-
if (!requireAttemptOne && !retainsAttemptTwo) return;
|
|
202
|
-
if (typeof read !== 'function') {
|
|
203
|
-
throw new TypeError(
|
|
204
|
-
'Canonical browser evidence validation requires an immutable package reader.',
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const readVerified = async (path) => {
|
|
209
|
-
const value = await read(path);
|
|
210
|
-
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
211
|
-
const expected = manifest.immutableHashes[path];
|
|
212
|
-
const actual = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
213
|
-
if (actual !== expected) {
|
|
214
|
-
throw new Error(`Immutable package hash mismatch for ${path}.`);
|
|
215
|
-
}
|
|
216
|
-
return bytes;
|
|
217
|
-
};
|
|
218
|
-
const builtArtifacts = (manifest.artifacts ?? []).filter(
|
|
219
|
-
({ status }) => status === 'built',
|
|
220
|
-
);
|
|
221
|
-
const browserRecords = new Map();
|
|
222
|
-
let expectedRuntimeHash;
|
|
223
|
-
let expectedCaptureIdentity;
|
|
224
|
-
for (const artifact of builtArtifacts) {
|
|
225
|
-
for (const viewport of REVIEW_VIEWPORTS) {
|
|
226
|
-
const metricsPath = `qa/browser/${artifact.id}/${viewport}.json`;
|
|
227
|
-
const bytes = await readVerified(metricsPath);
|
|
228
|
-
const screenshotPath = `qa/browser/${artifact.id}/${viewport}.png`;
|
|
229
|
-
const screenshotBytes = await readVerified(screenshotPath);
|
|
230
|
-
if (!isPng(screenshotBytes)) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Browser screenshot evidence ${screenshotPath} is not a PNG.`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const record = parseJson(bytes, metricsPath);
|
|
236
|
-
validateBrowserEvidenceRecord(record, {
|
|
237
|
-
artifactId: artifact.id,
|
|
238
|
-
viewport,
|
|
239
|
-
metricsPath,
|
|
240
|
-
requireLaunched: successful && runMode === 'unattended',
|
|
241
|
-
});
|
|
242
|
-
const runtimeHash = canonicalHash(record.runtime);
|
|
243
|
-
expectedRuntimeHash ??= runtimeHash;
|
|
244
|
-
expectedCaptureIdentity ??= record.captureIdentity;
|
|
245
|
-
if (
|
|
246
|
-
runtimeHash !== expectedRuntimeHash ||
|
|
247
|
-
record.captureIdentity !== expectedCaptureIdentity
|
|
248
|
-
) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`Browser runtime or capture identity mismatch in ${metricsPath}.`,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
browserRecords.set(metricsPath, { bytes, record, screenshotBytes });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const attempts = retainsAttemptTwo ? [1, 2] : [1];
|
|
258
|
-
let terminalResult;
|
|
259
|
-
for (const attempt of attempts) {
|
|
260
|
-
const root = `qa/visual-review/attempt-${attempt}`;
|
|
261
|
-
const requestPath = `${root}/request.json`;
|
|
262
|
-
const request = parseJson(await readVerified(requestPath), requestPath);
|
|
263
|
-
const requestValidation = validateContract(
|
|
264
|
-
'visual-review-request',
|
|
265
|
-
request,
|
|
266
|
-
);
|
|
267
|
-
if (!requestValidation.valid) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Immutable visual-review request ${requestPath} is invalid: ${requestValidation.errors
|
|
270
|
-
.map(({ code }) => code)
|
|
271
|
-
.join(', ')}.`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
if (
|
|
275
|
-
canonicalHash(request.browserRuntime) !== expectedRuntimeHash ||
|
|
276
|
-
request.captureIdentity !== expectedCaptureIdentity
|
|
277
|
-
) {
|
|
278
|
-
throw new Error(
|
|
279
|
-
`Visual-review browser runtime or capture identity mismatch in ${requestPath}.`,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
const requestEvidence = new Map(
|
|
283
|
-
request.renderedArtifacts.flatMap((artifact) =>
|
|
284
|
-
artifact.evidence.map((evidence) => [
|
|
285
|
-
`${artifact.artifactId}:${evidence.viewport}`,
|
|
286
|
-
evidence,
|
|
287
|
-
]),
|
|
288
|
-
),
|
|
289
|
-
);
|
|
290
|
-
for (const artifact of builtArtifacts) {
|
|
291
|
-
for (const viewport of REVIEW_VIEWPORTS) {
|
|
292
|
-
const key = `${artifact.id}:${viewport}`;
|
|
293
|
-
const evidence = requestEvidence.get(key);
|
|
294
|
-
const sourceMetricsPath = `qa/browser/${artifact.id}/${viewport}.json`;
|
|
295
|
-
const sourceScreenshotPath = `qa/browser/${artifact.id}/${viewport}.png`;
|
|
296
|
-
if (
|
|
297
|
-
!evidence ||
|
|
298
|
-
evidence.metricsPath !== sourceMetricsPath ||
|
|
299
|
-
evidence.screenshotPath !== sourceScreenshotPath ||
|
|
300
|
-
evidence.captureIdentity !== expectedCaptureIdentity
|
|
301
|
-
) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
`Visual-review evidence binding mismatch for ${key} in ${requestPath}.`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
const copiedMetricsPath = `${root}/evidence/${artifact.id}/${viewport}.json`;
|
|
307
|
-
const copiedMetrics = await readVerified(copiedMetricsPath);
|
|
308
|
-
const copiedScreenshotPath = `${root}/evidence/${artifact.id}/${viewport}.png`;
|
|
309
|
-
const copiedScreenshot = await readVerified(copiedScreenshotPath);
|
|
310
|
-
if (!isPng(copiedScreenshot)) {
|
|
311
|
-
throw new Error(
|
|
312
|
-
`Retained visual-review screenshot ${copiedScreenshotPath} is not a PNG.`,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
const copiedRecord = parseJson(copiedMetrics, copiedMetricsPath);
|
|
316
|
-
validateBrowserEvidenceRecord(copiedRecord, {
|
|
317
|
-
artifactId: artifact.id,
|
|
318
|
-
viewport,
|
|
319
|
-
metricsPath: copiedMetricsPath,
|
|
320
|
-
requireLaunched: successful && runMode === 'unattended',
|
|
321
|
-
});
|
|
322
|
-
if (
|
|
323
|
-
canonicalHash(copiedRecord.runtime) !== expectedRuntimeHash ||
|
|
324
|
-
copiedRecord.captureIdentity !== expectedCaptureIdentity
|
|
325
|
-
) {
|
|
326
|
-
throw new Error(
|
|
327
|
-
`Retained visual-review browser runtime or capture identity mismatch in ${copiedMetricsPath}.`,
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
if (
|
|
331
|
-
evidence.metricsHash !==
|
|
332
|
-
`sha256:${createHash('sha256').update(copiedMetrics).digest('hex')}`
|
|
333
|
-
) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
`Visual-review metrics hash mismatch for ${key} in ${requestPath}.`,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
if (
|
|
339
|
-
evidence.screenshotHash !==
|
|
340
|
-
`sha256:${createHash('sha256')
|
|
341
|
-
.update(copiedScreenshot)
|
|
342
|
-
.digest('hex')}`
|
|
343
|
-
) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`Visual-review screenshot hash mismatch for ${key} in ${requestPath}.`,
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
if (attempt === attempts.at(-1)) {
|
|
349
|
-
const sourceMetrics = browserRecords.get(sourceMetricsPath);
|
|
350
|
-
if (
|
|
351
|
-
!copiedMetrics.equals(sourceMetrics.bytes) ||
|
|
352
|
-
!copiedScreenshot.equals(sourceMetrics.screenshotBytes)
|
|
353
|
-
) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
`Terminal visual-review evidence differs from ${sourceMetricsPath}.`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (
|
|
362
|
-
requestEvidence.size !==
|
|
363
|
-
builtArtifacts.length * REVIEW_VIEWPORTS.length
|
|
364
|
-
) {
|
|
365
|
-
throw new Error(
|
|
366
|
-
`Visual-review evidence set in ${requestPath} does not match the complete browser record set.`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const resultPath = `${root}/result.json`;
|
|
371
|
-
const result = parseJson(await readVerified(resultPath), resultPath);
|
|
372
|
-
const resultValidation = validateContract(
|
|
373
|
-
'visual-review-evidence',
|
|
374
|
-
result,
|
|
375
|
-
{
|
|
376
|
-
visualReviewRequest: request,
|
|
377
|
-
attempt,
|
|
378
|
-
},
|
|
379
|
-
);
|
|
380
|
-
if (!resultValidation.valid) {
|
|
381
|
-
throw new Error(
|
|
382
|
-
`Immutable visual-review evidence ${resultPath} is invalid: ${resultValidation.errors
|
|
383
|
-
.map(({ code }) => code)
|
|
384
|
-
.join(', ')}.`,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
terminalResult = result;
|
|
388
|
-
}
|
|
389
|
-
if (successful && terminalResult?.disposition !== 'pass') {
|
|
390
|
-
throw new Error(
|
|
391
|
-
'Successful recap package requires a terminal passing visual review.',
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function addVisualReviewAttemptPaths(required, manifest, attempt) {
|
|
397
|
-
const root = `qa/visual-review/attempt-${attempt}`;
|
|
398
|
-
required.add(`${root}/request.json`);
|
|
399
|
-
required.add(`${root}/result.json`);
|
|
400
|
-
for (const artifact of manifest.artifacts ?? []) {
|
|
401
|
-
if (artifact.status !== 'built') continue;
|
|
402
|
-
for (const viewport of REVIEW_VIEWPORTS) {
|
|
403
|
-
required.add(`qa/browser/${artifact.id}/${viewport}.png`);
|
|
404
|
-
required.add(`qa/browser/${artifact.id}/${viewport}.json`);
|
|
405
|
-
required.add(`${root}/evidence/${artifact.id}/${viewport}.png`);
|
|
406
|
-
required.add(`${root}/evidence/${artifact.id}/${viewport}.json`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function isReviewMaterial(path) {
|
|
412
|
-
return path.startsWith('qa/browser/') || path.startsWith('qa/visual-review/');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function isLegacyReviewMaterial(path) {
|
|
416
|
-
return path.startsWith('qa/review-gate/');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function isAttemptTwoMaterial(path) {
|
|
420
|
-
return (
|
|
421
|
-
path === VISUAL_REVISION_PATH ||
|
|
422
|
-
path.startsWith('qa/visual-review/attempt-2/')
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function isObject(value) {
|
|
427
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
130
|
function assertInventoryPath(path) {
|
|
431
131
|
if (
|
|
432
132
|
typeof path !== 'string' ||
|
|
@@ -446,52 +146,6 @@ function assertInventoryPath(path) {
|
|
|
446
146
|
}
|
|
447
147
|
}
|
|
448
148
|
|
|
449
|
-
function
|
|
450
|
-
return (
|
|
451
|
-
bytes.length >= 8 &&
|
|
452
|
-
bytes
|
|
453
|
-
.subarray(0, 8)
|
|
454
|
-
.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function validateBrowserEvidenceRecord(
|
|
459
|
-
record,
|
|
460
|
-
{ artifactId, viewport, metricsPath, requireLaunched },
|
|
461
|
-
) {
|
|
462
|
-
if (
|
|
463
|
-
!isObject(record) ||
|
|
464
|
-
record.schemaVersion !== 'explainer-kit.browser-evidence/v2' ||
|
|
465
|
-
record.artifactId !== artifactId ||
|
|
466
|
-
record.viewport !== viewport ||
|
|
467
|
-
record.scenario !== 'default' ||
|
|
468
|
-
!isObject(record.runtime) ||
|
|
469
|
-
!['launched', 'fixture'].includes(record.runtime.kind) ||
|
|
470
|
-
record.runtime.name !== 'chromium' ||
|
|
471
|
-
typeof record.runtime.version !== 'string' ||
|
|
472
|
-
record.runtime.version.length === 0 ||
|
|
473
|
-
!isObject(record.capture) ||
|
|
474
|
-
!/^sha256:[a-f0-9]{64}$/.test(record.captureIdentity ?? '') ||
|
|
475
|
-
record.captureIdentity !==
|
|
476
|
-
browserCaptureIdentity(record.runtime, record.capture)
|
|
477
|
-
) {
|
|
478
|
-
throw new Error(
|
|
479
|
-
`Browser evidence ${metricsPath} does not satisfy explainer-kit.browser-evidence/v2.`,
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
if (requireLaunched && record.runtime.kind !== 'launched') {
|
|
483
|
-
throw new Error(
|
|
484
|
-
`Production browser evidence ${metricsPath} must identify launched Chromium, not a fixture session.`,
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function parseJson(bytes, path) {
|
|
490
|
-
try {
|
|
491
|
-
return JSON.parse(bytes.toString('utf8'));
|
|
492
|
-
} catch {
|
|
493
|
-
throw new Error(
|
|
494
|
-
`Immutable package record ${path} must contain valid JSON.`,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
149
|
+
function isObject(value) {
|
|
150
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
497
151
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
2
|
+
const REQUIRED_CHECKS = [
|
|
3
|
+
'parse',
|
|
4
|
+
'requiredNarrative',
|
|
5
|
+
'structure',
|
|
6
|
+
'sourceDumping',
|
|
7
|
+
'shellScripts',
|
|
8
|
+
'ledgerToPage',
|
|
9
|
+
'pageToLedger',
|
|
10
|
+
];
|
|
11
|
+
const SCREENSHOTS = ['qa/320.png', 'qa/768.png', 'qa/1440.png'];
|
|
12
|
+
const BROWSER_RUNGS = new Set(['host', 'playwright']);
|
|
13
|
+
|
|
14
|
+
export function validateQaResult(value) {
|
|
15
|
+
requireObject(value, 'QA result');
|
|
16
|
+
requireExactKeys(
|
|
17
|
+
value,
|
|
18
|
+
[
|
|
19
|
+
'artifactSha256',
|
|
20
|
+
'checks',
|
|
21
|
+
'rung',
|
|
22
|
+
'reason',
|
|
23
|
+
'screenshots',
|
|
24
|
+
'visual',
|
|
25
|
+
'deferredBrowserRequest',
|
|
26
|
+
],
|
|
27
|
+
['artifactSha256', 'checks', 'rung', 'visual'],
|
|
28
|
+
'QA result',
|
|
29
|
+
);
|
|
30
|
+
if (!HASH_PATTERN.test(value.artifactSha256)) {
|
|
31
|
+
throw new TypeError('QA result artifactSha256 is invalid.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
requireObject(value.checks, 'QA checks');
|
|
35
|
+
requireExactKeys(value.checks, REQUIRED_CHECKS, REQUIRED_CHECKS, 'QA checks');
|
|
36
|
+
for (const [id, check] of Object.entries(value.checks)) {
|
|
37
|
+
requireObject(check, `QA check ${id}`);
|
|
38
|
+
if (check.status === 'pass') {
|
|
39
|
+
requireExactKeys(check, ['status'], ['status'], `QA check ${id}`);
|
|
40
|
+
} else if (check.status === 'fail') {
|
|
41
|
+
requireExactKeys(
|
|
42
|
+
check,
|
|
43
|
+
['status', 'cause'],
|
|
44
|
+
['status', 'cause'],
|
|
45
|
+
`QA check ${id}`,
|
|
46
|
+
);
|
|
47
|
+
requireNonEmptyString(check.cause, `QA check ${id} cause`);
|
|
48
|
+
} else {
|
|
49
|
+
throw new TypeError(`QA check ${id} status is invalid.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (value.rung === 'none') {
|
|
54
|
+
requireExactKeys(
|
|
55
|
+
value.visual,
|
|
56
|
+
['verdict'],
|
|
57
|
+
['verdict'],
|
|
58
|
+
'QA visual result',
|
|
59
|
+
);
|
|
60
|
+
if (value.visual.verdict !== 'none') {
|
|
61
|
+
throw new TypeError('The none rung requires visual verdict none.');
|
|
62
|
+
}
|
|
63
|
+
requireNonEmptyString(value.reason, 'QA none-rung reason');
|
|
64
|
+
if (value.screenshots !== undefined) {
|
|
65
|
+
throw new TypeError('The none rung cannot claim screenshots.');
|
|
66
|
+
}
|
|
67
|
+
} else if (BROWSER_RUNGS.has(value.rung)) {
|
|
68
|
+
if (value.reason !== undefined) {
|
|
69
|
+
throw new TypeError('A browser rung cannot carry a runtime reason.');
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
!Array.isArray(value.screenshots) ||
|
|
73
|
+
value.screenshots.length !== SCREENSHOTS.length ||
|
|
74
|
+
!value.screenshots.every((path, index) => path === SCREENSHOTS[index])
|
|
75
|
+
) {
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
'A browser rung requires the three canonical screenshots.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
validateBrowserVisual(value.visual);
|
|
81
|
+
} else {
|
|
82
|
+
throw new TypeError('QA result rung is invalid.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (value.deferredBrowserRequest !== undefined) {
|
|
86
|
+
validateDeferredBrowserRequest(value.deferredBrowserRequest);
|
|
87
|
+
if (value.rung !== 'none') {
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
'A deferred browser request is valid only on the none rung.',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateBrowserVisual(visual) {
|
|
97
|
+
requireObject(visual, 'QA visual result');
|
|
98
|
+
if (visual.verdict === 'pass') {
|
|
99
|
+
requireExactKeys(
|
|
100
|
+
visual,
|
|
101
|
+
['verdict', 'notes'],
|
|
102
|
+
['verdict', 'notes'],
|
|
103
|
+
'QA visual result',
|
|
104
|
+
);
|
|
105
|
+
requireNonEmptyString(visual.notes, 'QA visual notes');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (visual.verdict === 'findings') {
|
|
109
|
+
requireExactKeys(
|
|
110
|
+
visual,
|
|
111
|
+
['verdict', 'findings', 'notes'],
|
|
112
|
+
['verdict', 'findings'],
|
|
113
|
+
'QA visual result',
|
|
114
|
+
);
|
|
115
|
+
if (
|
|
116
|
+
!Array.isArray(visual.findings) ||
|
|
117
|
+
visual.findings.length === 0 ||
|
|
118
|
+
!visual.findings.every(
|
|
119
|
+
(finding) => typeof finding === 'string' && finding.length > 0,
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError('QA visual findings are invalid.');
|
|
123
|
+
}
|
|
124
|
+
if (visual.notes !== undefined) {
|
|
125
|
+
requireNonEmptyString(visual.notes, 'QA visual notes');
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw new TypeError('QA visual verdict is invalid.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateDeferredBrowserRequest(value) {
|
|
133
|
+
requireObject(value, 'Deferred browser request');
|
|
134
|
+
requireExactKeys(
|
|
135
|
+
value,
|
|
136
|
+
['rung', 'screenshots', 'artifactSha256', 'visualVerdict', 'visualNotes'],
|
|
137
|
+
['rung', 'screenshots', 'artifactSha256', 'visualVerdict'],
|
|
138
|
+
'Deferred browser request',
|
|
139
|
+
);
|
|
140
|
+
if (value.rung !== 'host') {
|
|
141
|
+
throw new TypeError('Deferred browser request rung is invalid.');
|
|
142
|
+
}
|
|
143
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
144
|
+
requireNonEmptyString(entry, `Deferred browser request ${key}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function requireExactKeys(value, allowed, required, label) {
|
|
149
|
+
const keys = Object.keys(value);
|
|
150
|
+
if (
|
|
151
|
+
keys.some((key) => !allowed.includes(key)) ||
|
|
152
|
+
required.some((key) => !keys.includes(key))
|
|
153
|
+
) {
|
|
154
|
+
throw new TypeError(`${label} keys are invalid.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requireObject(value, label) {
|
|
159
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
160
|
+
throw new TypeError(`${label} must be an object.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function requireNonEmptyString(value, label) {
|
|
165
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
166
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
167
|
+
}
|
|
168
|
+
}
|