@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,648 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ import { writeFailure } from './bundle.mjs';
9
+ import {
10
+ createBrowserProbeSession,
11
+ RUNTIME_UNAVAILABLE_REASONS,
12
+ } from './lib/browser-runtime.mjs';
13
+ import {
14
+ harvestNumericTokens,
15
+ normalizeClaimSubject,
16
+ } from './lib/claim-subject.mjs';
17
+ import { validateHtmlSafety } from './lib/html-safety.mjs';
18
+ import {
19
+ BROWSER_PROBE_EVALUATE,
20
+ checkArtifactCohesion,
21
+ checkHtmlStructure,
22
+ checkSourceDumping,
23
+ pngDimensions,
24
+ REPRESENTATIVE_WIDTHS,
25
+ runBrowserProbes,
26
+ } from './lib/qa.mjs';
27
+ import { loadRecipe, recipeRequiredNarrative } from './lib/recipes.mjs';
28
+ import { sanitizeDiagnostic } from './lib/sanitize.mjs';
29
+
30
+ const STATUS_VALUES = new Set([
31
+ 'complete',
32
+ 'in_progress',
33
+ 'merged',
34
+ 'deferred',
35
+ 'parked',
36
+ 'wont_do',
37
+ 'closed',
38
+ 'open',
39
+ 'built',
40
+ 'built-needs-review',
41
+ 'failed',
42
+ 'incomplete',
43
+ 'skipped',
44
+ ]);
45
+ const COVERED_CLAIM_BLOCKS = new Set([
46
+ 'h1',
47
+ 'h2',
48
+ 'h3',
49
+ 'h4',
50
+ 'h5',
51
+ 'h6',
52
+ 'tr',
53
+ 'li',
54
+ 'p',
55
+ ]);
56
+ const CLAIM_LABEL_ELEMENTS = new Set([
57
+ 'h1',
58
+ 'h2',
59
+ 'h3',
60
+ 'h4',
61
+ 'h5',
62
+ 'h6',
63
+ 'dt',
64
+ 'label',
65
+ 'b',
66
+ 'strong',
67
+ ]);
68
+ const IGNORED_RESIDUAL_ELEMENTS = new Set(['script', 'style']);
69
+ const VOID_HTML_ELEMENTS = new Set([
70
+ 'area',
71
+ 'base',
72
+ 'br',
73
+ 'col',
74
+ 'embed',
75
+ 'hr',
76
+ 'img',
77
+ 'input',
78
+ 'link',
79
+ 'meta',
80
+ 'param',
81
+ 'source',
82
+ 'track',
83
+ 'wbr',
84
+ ]);
85
+
86
+ export function extractRenderedClaims(html) {
87
+ const terminology = {};
88
+ const numericClaims = {};
89
+ const statuses = {};
90
+ const claims = [];
91
+ const seenClaims = new Set();
92
+ const sections = extractSections(html);
93
+
94
+ function harvestValues(valuesText, subject) {
95
+ const addClaim = (value, kind) => {
96
+ const key = `${subject}\0${value}\0${kind}`;
97
+ if (seenClaims.has(key)) return;
98
+ seenClaims.add(key);
99
+ if (kind === 'status') {
100
+ statuses[subject] = value;
101
+ } else {
102
+ numericClaims[subject] = value;
103
+ }
104
+ claims.push({ subject, value, kind });
105
+ };
106
+
107
+ for (const value of valuesText.match(/\b\d{4}-\d{2}-\d{2}\b/g) ?? []) {
108
+ addClaim(value, 'date');
109
+ }
110
+ const withoutDates = valuesText.replace(/\b\d{4}-\d{2}-\d{2}\b/g, '');
111
+ for (const value of harvestNumericTokens(withoutDates)) {
112
+ addClaim(value, 'number');
113
+ }
114
+ for (const token of valuesText.toLowerCase().match(/[a-z][a-z_-]*/g) ??
115
+ []) {
116
+ if (STATUS_VALUES.has(token)) addClaim(token, 'status');
117
+ }
118
+ }
119
+
120
+ for (const section of sections) {
121
+ const blockPattern = /<(h[1-6]|tr|li|p)\b[^>]*>([\s\S]*?)<\/\1>/gi;
122
+ for (const match of section.html.matchAll(blockPattern)) {
123
+ const [, tag, blockHtml] = match;
124
+ const text = htmlText(blockHtml);
125
+ if (!text) continue;
126
+ const isHeading = tag.toLowerCase().startsWith('h');
127
+ if (isHeading) {
128
+ terminology[text] = text;
129
+ }
130
+ const rowCells =
131
+ tag.toLowerCase() === 'tr'
132
+ ? [...blockHtml.matchAll(/<t[hd]\b[^>]*>([\s\S]*?)<\/t[hd]>/gi)]
133
+ .map((cell) => htmlText(cell[1]))
134
+ .filter(Boolean)
135
+ : [];
136
+ const subject = normalizeClaimSubject({
137
+ text,
138
+ rowSubject: isHeading ? text : rowCells[0],
139
+ sectionId: section.id,
140
+ });
141
+ const valuesText =
142
+ rowCells.length > 1 ? rowCells.slice(1).join(' ') : text;
143
+ harvestValues(valuesText, subject);
144
+ }
145
+
146
+ for (const residual of extractResidualClaimSegments(
147
+ section.html,
148
+ section.id,
149
+ )) {
150
+ harvestValues(residual.text, residual.subject);
151
+ }
152
+ }
153
+ return { terminology, numericClaims, statuses, claims };
154
+ }
155
+
156
+ export function containsLedgerTerm(text, term) {
157
+ if (
158
+ typeof text !== 'string' ||
159
+ typeof term !== 'string' ||
160
+ term.length === 0
161
+ ) {
162
+ return false;
163
+ }
164
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
165
+ return new RegExp(`(?<![A-Za-z0-9_-])${escaped}(?![A-Za-z0-9_-])`).test(text);
166
+ }
167
+
168
+ function extractResidualClaimSegments(html, sectionId) {
169
+ const segments = [];
170
+ const stack = [
171
+ {
172
+ tag: '#section',
173
+ subject: null,
174
+ labelText: null,
175
+ coveredContext: false,
176
+ },
177
+ ];
178
+ let coveredDepth = 0;
179
+ let ignoredDepth = 0;
180
+
181
+ const nearestSubject = () =>
182
+ stack.findLast((frame) => frame.subject)?.subject ?? sectionId;
183
+ const addSegment = (text, subject = nearestSubject()) => {
184
+ const normalized = htmlText(text);
185
+ if (normalized) {
186
+ segments.push({ text: normalized, subject });
187
+ }
188
+ };
189
+ const closeFrame = () => {
190
+ const frame = stack.pop();
191
+ if (!frame || frame.tag === '#section') return;
192
+
193
+ if (frame.labelText !== null) {
194
+ const labelText = htmlText(frame.labelText);
195
+ const subject = normalizeClaimSubject({
196
+ text: labelText,
197
+ sectionId: null,
198
+ });
199
+ if (subject) {
200
+ stack.at(-1).subject = subject;
201
+ if (!frame.coveredContext && ignoredDepth === 0) {
202
+ addSegment(frame.labelText, subject);
203
+ }
204
+ } else if (!frame.coveredContext && ignoredDepth === 0) {
205
+ addSegment(frame.labelText);
206
+ }
207
+ }
208
+ if (frame.covered) coveredDepth -= 1;
209
+ if (frame.ignored) ignoredDepth -= 1;
210
+ };
211
+
212
+ for (const match of String(html).matchAll(
213
+ /<!--[\s\S]*?-->|<![^>]*>|<\/?[a-z][^>]*>|[^<]+/gi,
214
+ )) {
215
+ const token = match[0];
216
+ const close = token.match(/^<\/([a-z][\w:-]*)\s*>/i);
217
+ if (close) {
218
+ while (stack.length > 1 && stack.at(-1).tag !== close[1].toLowerCase()) {
219
+ closeFrame();
220
+ }
221
+ if (stack.length > 1) closeFrame();
222
+ continue;
223
+ }
224
+
225
+ const open = token.match(/^<([a-z][\w:-]*)\b/i);
226
+ if (open) {
227
+ const tag = open[1].toLowerCase();
228
+ const covered = COVERED_CLAIM_BLOCKS.has(tag);
229
+ const ignored = IGNORED_RESIDUAL_ELEMENTS.has(tag);
230
+ stack.push({
231
+ tag,
232
+ subject: null,
233
+ labelText: CLAIM_LABEL_ELEMENTS.has(tag) ? '' : null,
234
+ covered,
235
+ ignored,
236
+ coveredContext: coveredDepth > 0 || covered,
237
+ });
238
+ if (covered) coveredDepth += 1;
239
+ if (ignored) ignoredDepth += 1;
240
+ if (VOID_HTML_ELEMENTS.has(tag) || /\/\s*>$/.test(token)) closeFrame();
241
+ continue;
242
+ }
243
+
244
+ if (ignoredDepth > 0) continue;
245
+ const labels = stack.filter((frame) => frame.labelText !== null);
246
+ if (labels.length > 0) {
247
+ for (const label of labels) label.labelText += token;
248
+ } else if (coveredDepth === 0) {
249
+ addSegment(token);
250
+ }
251
+ }
252
+
253
+ while (stack.length > 1) closeFrame();
254
+ return segments;
255
+ }
256
+
257
+ export async function verifyRun({
258
+ runRoot,
259
+ recipe,
260
+ rung = 'none',
261
+ screenshots,
262
+ artifactSha256,
263
+ visualVerdict,
264
+ visualNotes,
265
+ headlessRuntimeOptions,
266
+ }) {
267
+ const pagePath = join(runRoot, 'site/index.html');
268
+ let html;
269
+ try {
270
+ html = await readFile(pagePath, 'utf8');
271
+ } catch (error) {
272
+ if (error.code === 'ENOENT') {
273
+ await rm(join(runRoot, 'qa/result.json'), { force: true });
274
+ await writeFailure(runRoot, 'authoring', 'site/index.html is missing.');
275
+ throw verifyError(
276
+ 'verify-authoring-missing',
277
+ 'site/index.html is missing.',
278
+ );
279
+ }
280
+ throw error;
281
+ }
282
+
283
+ try {
284
+ const recipeVersion = recipe === 'project-recap' ? '2' : '1';
285
+ const recipeContract = loadRecipe(recipe, recipeVersion);
286
+ const artifact = recipeContract.floor[0];
287
+ const [ledger, factBase, shell] = await Promise.all([
288
+ readJson(join(runRoot, 'source/ledger.json')),
289
+ readJson(join(runRoot, 'source/fact-base.json')),
290
+ readFile(
291
+ new URL(`../templates/${artifact.template}.html`, import.meta.url),
292
+ 'utf8',
293
+ ),
294
+ ]);
295
+ const requiredSections = recipeRequiredNarrative(
296
+ recipeContract,
297
+ artifact.id,
298
+ );
299
+ const sections = extractSections(html);
300
+ const sectionById = new Map(
301
+ sections.map((section) => [section.id, section]),
302
+ );
303
+ const structure = checkHtmlStructure({
304
+ id: artifact.id,
305
+ type: artifact.type,
306
+ html,
307
+ });
308
+ const safety = validateHtmlSafety({
309
+ html,
310
+ shell,
311
+ shellName: artifact.template,
312
+ });
313
+ const sourceDumping = checkSourceDumping({
314
+ authoredText: htmlText(html),
315
+ authoredSections: sections.map(({ id, html: sectionHtml }) => ({
316
+ id,
317
+ text: htmlText(sectionHtml),
318
+ })),
319
+ sourceTexts: [
320
+ ...factBase.claims.map(({ text }) => text),
321
+ ...factBase.unresolvedClaims.map(({ text }) => text),
322
+ ],
323
+ });
324
+ const renderedClaims = extractRenderedClaims(html);
325
+ const plainText = htmlText(html);
326
+ for (const { term } of ledger.terminology ?? []) {
327
+ if (containsLedgerTerm(plainText, term)) {
328
+ renderedClaims.terminology[term] = term;
329
+ }
330
+ }
331
+ const cohesion = checkArtifactCohesion(
332
+ [{ id: artifact.id, cohesion: renderedClaims }],
333
+ { ledger },
334
+ );
335
+ const expectedClaims = new Set(
336
+ (ledger.claims ?? []).map(
337
+ ({ subject, value, kind }) => `${subject}\0${value}\0${kind}`,
338
+ ),
339
+ );
340
+ const untracedClaims = renderedClaims.claims.filter(
341
+ ({ subject, value, kind }) =>
342
+ !expectedClaims.has(`${subject}\0${value}\0${kind}`),
343
+ );
344
+ const missingSections = requiredSections.filter((id) => {
345
+ const section = sectionById.get(id);
346
+ return !section || htmlText(section.html).length === 0;
347
+ });
348
+ const parseIssues = [
349
+ ...structure.issues.filter(({ code }) => code === 'tag-balance'),
350
+ ...safety.errors.filter((code) => code === 'malformed-html'),
351
+ ];
352
+ const checks = {
353
+ parse: checkResult(parseIssues),
354
+ requiredNarrative: checkResult(
355
+ missingSections.map((id) => `required-section-missing-or-empty:${id}`),
356
+ ),
357
+ structure: checkResult(
358
+ structure.issues.filter(({ code }) => code !== 'tag-balance'),
359
+ ),
360
+ sourceDumping: checkResult(sourceDumping.issues),
361
+ shellScripts: checkResult(
362
+ safety.errors.filter((code) => code !== 'malformed-html'),
363
+ ),
364
+ ledgerToPage: checkResult(cohesion.issues),
365
+ pageToLedger: checkResult(
366
+ untracedClaims.map(
367
+ ({ subject, value }) => `verify-claim-untraced:${subject}:${value}`,
368
+ ),
369
+ ),
370
+ };
371
+ const pageHash = hashText(html);
372
+ const browserSafetyPassed = ['parse', 'structure', 'shellScripts'].every(
373
+ (check) => checks[check].status === 'pass',
374
+ );
375
+ const browserResult =
376
+ !browserSafetyPassed && rung !== 'none'
377
+ ? await downgradeToNone(
378
+ runRoot,
379
+ 'browser-blocked-by-static-safety-checks',
380
+ )
381
+ : rung === 'host'
382
+ ? await verifyHostRung({
383
+ runRoot,
384
+ screenshots,
385
+ expectedArtifactHash: pageHash,
386
+ artifactSha256,
387
+ visualVerdict,
388
+ visualNotes,
389
+ })
390
+ : rung === 'playwright'
391
+ ? await verifyPlaywrightRung({
392
+ runRoot,
393
+ artifact: { ...artifact, html },
394
+ headlessRuntimeOptions,
395
+ })
396
+ : await downgradeToNone(
397
+ runRoot,
398
+ RUNTIME_UNAVAILABLE_REASONS.disabled,
399
+ );
400
+ const result = {
401
+ artifactSha256: pageHash,
402
+ checks,
403
+ ...browserResult,
404
+ };
405
+ await mkdir(join(runRoot, 'qa'), { recursive: true });
406
+ await rm(join(runRoot, 'failure.json'), { force: true });
407
+ await writeFile(
408
+ join(runRoot, 'qa/result.json'),
409
+ `${JSON.stringify(result, null, 2)}\n`,
410
+ );
411
+ return result;
412
+ } catch (error) {
413
+ if (error.code?.startsWith('verify-')) throw error;
414
+ await writeFailure(runRoot, 'verify', error);
415
+ throw error;
416
+ }
417
+ }
418
+
419
+ export async function runVerify(argv, io = console) {
420
+ const options = parseArgs(argv);
421
+ const result = await verifyRun(options);
422
+ io.log(JSON.stringify({ runRoot: options.runRoot, rung: result.rung }));
423
+ return result;
424
+ }
425
+
426
+ function extractSections(html) {
427
+ const sections = [];
428
+ for (const match of html.matchAll(
429
+ /<section\b[^>]*\bid\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/section>/gi,
430
+ )) {
431
+ sections.push({ id: match[1], html: match[2] });
432
+ }
433
+ return sections;
434
+ }
435
+
436
+ function htmlText(html) {
437
+ return String(html)
438
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
439
+ .replace(/<[^>]+>/g, ' ')
440
+ .replace(/&(?:nbsp|#160);/gi, ' ')
441
+ .replace(/&amp;/gi, '&')
442
+ .replace(/&lt;/gi, '<')
443
+ .replace(/&gt;/gi, '>')
444
+ .replace(/&quot;/gi, '"')
445
+ .replace(/&#39;|&apos;/gi, "'")
446
+ .replace(/\s+/g, ' ')
447
+ .trim();
448
+ }
449
+
450
+ function checkResult(issues) {
451
+ return issues.length === 0
452
+ ? { status: 'pass' }
453
+ : {
454
+ status: 'fail',
455
+ cause: issues
456
+ .map((issue) =>
457
+ typeof issue === 'string'
458
+ ? issue
459
+ : `${issue.code}:${issue.message}`,
460
+ )
461
+ .join('; '),
462
+ };
463
+ }
464
+
465
+ async function verifyHostRung({
466
+ runRoot,
467
+ screenshots,
468
+ expectedArtifactHash,
469
+ artifactSha256,
470
+ visualVerdict,
471
+ visualNotes,
472
+ }) {
473
+ if (!['pass', 'findings'].includes(visualVerdict)) {
474
+ throw verifyError(
475
+ 'verify-host-visual-verdict-required',
476
+ 'The host rung requires --visual-verdict pass|findings after screenshot inspection.',
477
+ );
478
+ }
479
+ if (artifactSha256 !== expectedArtifactHash) {
480
+ return downgradeToNone(runRoot, 'host-artifact-hash-mismatch');
481
+ }
482
+
483
+ const captures = await readHostScreenshots(screenshots);
484
+ if (!captures) {
485
+ return downgradeToNone(runRoot, 'host-screenshot-invalid');
486
+ }
487
+ await clearCanonicalScreenshots(runRoot);
488
+ await mkdir(join(runRoot, 'qa'), { recursive: true });
489
+ await Promise.all(
490
+ captures.map(({ path, bytes }) => writeFile(join(runRoot, path), bytes)),
491
+ );
492
+
493
+ const notes =
494
+ visualNotes?.trim() ||
495
+ `Host inspection reported ${visualVerdict} at 320, 768, and 1440 pixels.`;
496
+ return {
497
+ rung: 'host',
498
+ screenshots: captures.map(({ path }) => path),
499
+ visual: {
500
+ verdict: visualVerdict,
501
+ ...(visualVerdict === 'findings' && { findings: [notes] }),
502
+ notes,
503
+ },
504
+ };
505
+ }
506
+
507
+ async function verifyPlaywrightRung({
508
+ runRoot,
509
+ artifact,
510
+ headlessRuntimeOptions,
511
+ }) {
512
+ let session;
513
+ try {
514
+ session = await createBrowserProbeSession(headlessRuntimeOptions);
515
+ } catch (error) {
516
+ return downgradeToNone(
517
+ runRoot,
518
+ `playwright-launch-failed:${sanitizeDiagnostic(error)}`,
519
+ );
520
+ }
521
+ if (!session.available) return downgradeToNone(runRoot, session.reason);
522
+
523
+ try {
524
+ await clearCanonicalScreenshots(runRoot);
525
+ const probes = await runBrowserProbes({
526
+ artifacts: [artifact],
527
+ probe: (request) =>
528
+ session.probe({
529
+ ...request,
530
+ evaluate: BROWSER_PROBE_EVALUATE,
531
+ ...(request.scenario === 'default' && {
532
+ screenshotPath: join(runRoot, `qa/${request.viewport.width}.png`),
533
+ }),
534
+ }),
535
+ });
536
+ const screenshots = REPRESENTATIVE_WIDTHS.map((width) => `qa/${width}.png`);
537
+ const findings = probes.issues.map(formatBrowserFinding);
538
+ return {
539
+ rung: 'playwright',
540
+ screenshots,
541
+ visual:
542
+ findings.length === 0
543
+ ? {
544
+ verdict: 'pass',
545
+ notes: 'Playwright probes passed at all representative widths.',
546
+ }
547
+ : { verdict: 'findings', findings },
548
+ };
549
+ } catch (error) {
550
+ return downgradeToNone(
551
+ runRoot,
552
+ `playwright-probe-failed:${sanitizeDiagnostic(error)}`,
553
+ );
554
+ } finally {
555
+ await session.close();
556
+ }
557
+ }
558
+
559
+ function formatBrowserFinding({ code, width, scenario, message }) {
560
+ return [code, width && `${width}px`, scenario, message]
561
+ .filter(Boolean)
562
+ .join(':');
563
+ }
564
+
565
+ async function readHostScreenshots(screenshots) {
566
+ if (typeof screenshots !== 'string' || screenshots.length === 0) return null;
567
+ try {
568
+ const captures = await Promise.all(
569
+ REPRESENTATIVE_WIDTHS.map(async (width) => {
570
+ const bytes = await readFile(join(screenshots, `${width}.png`));
571
+ const dimensions = pngDimensions(bytes);
572
+ if (!dimensions || dimensions.width !== width) {
573
+ throw new Error('invalid screenshot');
574
+ }
575
+ return { path: `qa/${width}.png`, bytes };
576
+ }),
577
+ );
578
+ return captures;
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+
584
+ async function clearCanonicalScreenshots(runRoot) {
585
+ await Promise.all(
586
+ REPRESENTATIVE_WIDTHS.map((width) =>
587
+ rm(join(runRoot, `qa/${width}.png`), { force: true }),
588
+ ),
589
+ );
590
+ }
591
+
592
+ async function downgradeToNone(runRoot, reason) {
593
+ await clearCanonicalScreenshots(runRoot);
594
+ return {
595
+ rung: 'none',
596
+ reason,
597
+ visual: { verdict: 'none' },
598
+ };
599
+ }
600
+
601
+ function parseArgs(argv) {
602
+ const options = { rung: 'none' };
603
+ for (let index = 0; index < argv.length; index += 2) {
604
+ const field = {
605
+ '--run-root': 'runRoot',
606
+ '--recipe': 'recipe',
607
+ '--rung': 'rung',
608
+ '--screenshots': 'screenshots',
609
+ '--artifact-sha256': 'artifactSha256',
610
+ '--visual-verdict': 'visualVerdict',
611
+ '--visual-notes': 'visualNotes',
612
+ }[argv[index]];
613
+ if (!field || !argv[index + 1]) throw usageError();
614
+ options[field] = argv[index + 1];
615
+ }
616
+ if (!options.runRoot || !options.recipe) throw usageError();
617
+ if (!['none', 'host', 'playwright'].includes(options.rung))
618
+ throw usageError();
619
+ return options;
620
+ }
621
+
622
+ async function readJson(path) {
623
+ return JSON.parse(await readFile(path, 'utf8'));
624
+ }
625
+
626
+ function hashText(text) {
627
+ return `sha256:${createHash('sha256').update(text).digest('hex')}`;
628
+ }
629
+
630
+ function usageError() {
631
+ return verifyError(
632
+ 'verify-usage',
633
+ 'Usage: verify.mjs --run-root <dir> --recipe <id> [--rung none|playwright | --rung host --screenshots <dir> --artifact-sha256 <hash> --visual-verdict pass|findings [--visual-notes <text>]]',
634
+ );
635
+ }
636
+
637
+ function verifyError(code, message) {
638
+ const error = new Error(message);
639
+ error.code = code;
640
+ return error;
641
+ }
642
+
643
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
644
+ runVerify(process.argv.slice(2)).catch((error) => {
645
+ console.error(`${error.code ?? 'verify-error'}: ${error.message}`);
646
+ process.exitCode = 1;
647
+ });
648
+ }