@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.
Files changed (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. 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
- import { browserCaptureIdentity } from './browser-runtime.mjs';
6
- import { canonicalHash, validateContract } from './contracts.mjs';
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 const PACKAGE_COVERAGE_VERSION = 'explainer-kit.package-coverage/v2';
9
- export const SET_PLAN_RECORD_PATHS = Object.freeze([
10
- 'source/set-plan/request.json',
11
- 'source/set-plan/result.json',
12
- 'source/set-plan/ledger.json',
13
- 'source/set-plan/portfolio.json',
14
- 'source/set-plan/drafts.json',
15
- ]);
16
- export const VISUAL_REVISION_PATH = 'qa/visual-review/revision.json';
17
-
18
- const REVIEW_VIEWPORTS = Object.freeze(['mobile', 'tablet', 'desktop']);
19
- const SUCCESSFUL_OUTCOMES = new Set(['built-not-durable', 'built-durable']);
20
- const PARTIAL_REVIEW_OUTCOME = 'built-needs-review';
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
- ...Object.keys(manifest.immutableHashes),
33
- 'build-record.json',
34
- ...(includeManifest ? ['manifest.json'] : []),
35
- ...(includeTerminalEvidence ? ['terminal-evidence.json'] : []),
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
- const allowedFiles = new Set(
60
- permissibleRunPackagePaths(manifest, {
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 isPng(bytes) {
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
+ }