@open-agent-toolkit/cli 0.2.72 → 0.2.74

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