@open-agent-toolkit/cli 0.2.25 → 0.2.27

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 (87) hide show
  1. package/assets/NOTICES.md +156 -0
  2. package/assets/docs/cli-utilities/configuration.md +42 -11
  3. package/assets/docs/contributing/explainer-kit-verification.md +125 -0
  4. package/assets/docs/contributing/index.md +1 -0
  5. package/assets/docs/reference/troubleshooting.md +47 -0
  6. package/assets/docs/workflows/projects/artifacts.md +24 -6
  7. package/assets/docs/workflows/projects/dispatch-ceiling.md +67 -16
  8. package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
  9. package/assets/docs/workflows/skills/explainer-kit.md +121 -69
  10. package/assets/docs/workflows/skills/index.md +1 -0
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/explainer-kit/SKILL.md +18 -3
  13. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
  14. package/assets/skills/explainer-kit/references/contracts.md +167 -20
  15. package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
  16. package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
  17. package/assets/skills/explainer-kit/references/visual-review.md +57 -0
  18. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
  19. package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
  20. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
  21. package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
  22. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
  23. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
  24. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
  25. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
  26. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
  27. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
  28. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
  29. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
  30. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
  31. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
  32. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
  33. package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
  34. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
  35. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
  36. package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
  37. package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
  38. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
  39. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
  40. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
  41. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
  42. package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
  43. package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
  44. package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
  45. package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
  46. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
  47. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
  48. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
  49. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
  50. package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
  51. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  53. package/assets/skills/oat-project-implement/SKILL.md +9 -11
  54. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +18 -9
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  56. package/assets/skills/oat-project-implement/references/phase-execution.md +13 -4
  57. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  58. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  59. package/dist/commands/config/index.d.ts.map +1 -1
  60. package/dist/commands/config/index.js +27 -3
  61. package/dist/commands/project/archive/archive-utils.d.ts +1 -0
  62. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  63. package/dist/commands/project/archive/archive-utils.js +109 -42
  64. package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
  65. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
  66. package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
  67. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
  68. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
  69. package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
  70. package/dist/commands/project/archive/push-runner.d.ts +2 -1
  71. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  72. package/dist/commands/project/archive/push-runner.js +5 -1
  73. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  74. package/dist/commands/project/dispatch-ceiling/index.js +90 -0
  75. package/dist/config/dispatch-notices.d.ts +8 -0
  76. package/dist/config/dispatch-notices.d.ts.map +1 -0
  77. package/dist/config/dispatch-notices.js +79 -0
  78. package/dist/config/dispatch-policy-options.d.ts +2 -0
  79. package/dist/config/dispatch-policy-options.d.ts.map +1 -1
  80. package/dist/config/dispatch-policy-options.js +14 -2
  81. package/dist/providers/identity/dispatch-report.d.ts +17 -0
  82. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  83. package/dist/providers/identity/dispatch-report.js +30 -0
  84. package/dist/release/public-package-contract.d.ts +6 -0
  85. package/dist/release/public-package-contract.d.ts.map +1 -1
  86. package/dist/release/public-package-contract.js +75 -0
  87. package/package.json +2 -2
@@ -0,0 +1,380 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { isAbsolute, relative, resolve } from 'node:path';
4
+
5
+ import {
6
+ canonicalHash,
7
+ validateContract,
8
+ visualReviewRequestId,
9
+ } from './contracts.mjs';
10
+ import { decodeBrowserPng } from './png.mjs';
11
+
12
+ const REQUIRED_VIEWPORTS = Object.freeze(['mobile', 'tablet', 'desktop']);
13
+
14
+ export async function runVisualReview({
15
+ plan,
16
+ rendered,
17
+ evidence,
18
+ visualCritic,
19
+ runRoot,
20
+ }) {
21
+ if (typeof visualCritic !== 'function') {
22
+ throw visualReviewError(
23
+ 'Independent visual review requires a visualCritic callback.',
24
+ );
25
+ }
26
+ const { request, snapshots } = await buildVisualReviewRequest({
27
+ plan,
28
+ rendered,
29
+ evidence,
30
+ runRoot,
31
+ });
32
+ const evidenceInput = Object.freeze({
33
+ read: async (path) => {
34
+ const snapshot = snapshots.get(path);
35
+ if (!snapshot) {
36
+ throw visualReviewError(
37
+ `Visual critic requested unbound evidence path ${path}.`,
38
+ );
39
+ }
40
+ return Buffer.from(snapshot.bytes);
41
+ },
42
+ });
43
+ const result = await visualCritic(structuredClone(request), evidenceInput);
44
+ await assertSnapshotsUnchanged(runRoot, snapshots);
45
+ const validation = validateContract('visual-review-result', result, {
46
+ visualReviewRequest: request,
47
+ });
48
+ if (!validation.valid) {
49
+ throw visualReviewError(
50
+ `Visual critic returned an invalid result: ${formatErrors(validation.errors)}`,
51
+ );
52
+ }
53
+ return {
54
+ request,
55
+ result: structuredClone(result),
56
+ };
57
+ }
58
+
59
+ export async function buildVisualReviewRequest({
60
+ plan,
61
+ rendered,
62
+ evidence,
63
+ runRoot,
64
+ }) {
65
+ if (!Array.isArray(rendered) || !Array.isArray(evidence)) {
66
+ throw visualReviewError(
67
+ 'Visual review requires rendered artifacts and browser evidence arrays.',
68
+ );
69
+ }
70
+ if (typeof runRoot !== 'string' || runRoot.length === 0) {
71
+ throw visualReviewError('Visual review requires a confined run root.');
72
+ }
73
+ const browserBinding = browserBindingFor(evidence);
74
+ const paths = [
75
+ ...rendered.map(({ renderedPath }) => renderedPath),
76
+ ...evidence.flatMap(({ screenshotPath, metricsPath }) => [
77
+ screenshotPath,
78
+ metricsPath,
79
+ ]),
80
+ ];
81
+ const snapshots = new Map(
82
+ await Promise.all(
83
+ [...new Set(paths)].map(async (path) => [
84
+ path,
85
+ await captureSnapshot(runRoot, path),
86
+ ]),
87
+ ),
88
+ );
89
+ for (const item of evidence) {
90
+ const snapshot = snapshots.get(item.screenshotPath);
91
+ let decoded;
92
+ try {
93
+ decoded = decodeBrowserPng(snapshot.bytes);
94
+ } catch (error) {
95
+ throw visualReviewError(
96
+ `Visual review screenshot ${item.screenshotPath} is not a decodable browser PNG: ${error.message}`,
97
+ );
98
+ }
99
+ if (
100
+ typeof item.decodedScreenshotHash !== 'string' ||
101
+ decoded.decodedHash !== item.decodedScreenshotHash
102
+ ) {
103
+ throw visualReviewError(
104
+ `Visual review screenshot ${item.screenshotPath} does not match its decoded screenshot hash.`,
105
+ );
106
+ }
107
+ }
108
+ const payload = {
109
+ schemaVersion: 'explainer-kit.visual-review-request/v1',
110
+ browserRuntime: browserBinding.runtime,
111
+ captureIdentity: browserBinding.captureIdentity,
112
+ plan: structuredClone(plan),
113
+ renderedArtifacts: rendered.map(({ artifactId, renderedPath }) => ({
114
+ artifactId,
115
+ renderedPath,
116
+ renderedHash: snapshots.get(renderedPath).hash,
117
+ cohesionObservations: cohesionObservationsFromLedger({
118
+ artifactId,
119
+ content: snapshots.get(renderedPath).bytes,
120
+ contentHash: snapshots.get(renderedPath).hash,
121
+ ledger: plan?.ledger,
122
+ }),
123
+ evidence: evidence
124
+ .filter((item) => item.artifactId === artifactId)
125
+ .map(({ viewport, screenshotPath, metricsPath, captureIdentity }) => ({
126
+ viewport,
127
+ screenshotPath,
128
+ screenshotHash: snapshots.get(screenshotPath).hash,
129
+ metricsPath,
130
+ metricsHash: snapshots.get(metricsPath).hash,
131
+ captureIdentity,
132
+ })),
133
+ })),
134
+ };
135
+ const requestHash = canonicalHash(payload);
136
+ const request = {
137
+ ...payload,
138
+ requestId: visualReviewRequestId(requestHash),
139
+ requestHash,
140
+ };
141
+ const validation = validateContract('visual-review-request', request);
142
+ if (!validation.valid) {
143
+ throw visualReviewError(
144
+ `Visual review request is incomplete: ${formatErrors(validation.errors)}`,
145
+ );
146
+ }
147
+ for (const artifact of request.renderedArtifacts) {
148
+ const viewports = new Set(
149
+ artifact.evidence.map(({ viewport }) => viewport),
150
+ );
151
+ if (
152
+ viewports.size !== REQUIRED_VIEWPORTS.length ||
153
+ REQUIRED_VIEWPORTS.some((viewport) => !viewports.has(viewport))
154
+ ) {
155
+ throw visualReviewError(
156
+ `Visual review requires mobile, tablet, and desktop evidence for ${artifact.artifactId}.`,
157
+ );
158
+ }
159
+ }
160
+ return { request, snapshots };
161
+ }
162
+
163
+ function browserBindingFor(evidence) {
164
+ if (evidence.length === 0) {
165
+ throw visualReviewError(
166
+ 'Visual review requires trusted browser runtime evidence.',
167
+ );
168
+ }
169
+ const first = evidence[0];
170
+ if (
171
+ !validBrowserRuntime(first.runtime) ||
172
+ !/^sha256:[a-f0-9]{64}$/.test(first.captureIdentity ?? '')
173
+ ) {
174
+ throw visualReviewError(
175
+ 'Visual review evidence is missing trusted browser runtime identity.',
176
+ );
177
+ }
178
+ const runtimeHash = canonicalHash(first.runtime);
179
+ for (const item of evidence) {
180
+ if (
181
+ !validBrowserRuntime(item.runtime) ||
182
+ canonicalHash(item.runtime) !== runtimeHash ||
183
+ item.captureIdentity !== first.captureIdentity
184
+ ) {
185
+ throw visualReviewError(
186
+ 'Visual review browser runtime or capture identity does not match across evidence records.',
187
+ );
188
+ }
189
+ }
190
+ return {
191
+ runtime: structuredClone(first.runtime),
192
+ captureIdentity: first.captureIdentity,
193
+ };
194
+ }
195
+
196
+ function validBrowserRuntime(runtime) {
197
+ return (
198
+ runtime &&
199
+ typeof runtime === 'object' &&
200
+ ['launched', 'fixture'].includes(runtime.kind) &&
201
+ runtime.name === 'chromium' &&
202
+ typeof runtime.version === 'string' &&
203
+ runtime.version.length > 0
204
+ );
205
+ }
206
+
207
+ export function cohesionEvidenceFromLedger(artifacts, plan) {
208
+ if (!plan?.ledger) return artifacts;
209
+ return artifacts.map((artifact) => {
210
+ const text = visibleText(artifact.html);
211
+ return {
212
+ ...artifact,
213
+ cohesion: {
214
+ terminology: observedClaims(
215
+ plan.ledger.terminology,
216
+ ({ term }) => term,
217
+ ({ term }) => term,
218
+ text,
219
+ ),
220
+ numericClaims: observedClaims(
221
+ plan.ledger.numbers,
222
+ ({ subject }) => subject,
223
+ ({ value }) => value,
224
+ text,
225
+ true,
226
+ ),
227
+ statuses: observedClaims(
228
+ plan.ledger.statuses,
229
+ ({ subject }) => subject,
230
+ ({ value }) => value,
231
+ text,
232
+ ),
233
+ },
234
+ };
235
+ });
236
+ }
237
+
238
+ export function cohesionObservationsFromLedger({
239
+ artifactId,
240
+ content,
241
+ contentHash,
242
+ ledger,
243
+ }) {
244
+ const text = visibleText(
245
+ Buffer.isBuffer(content) ? content.toString() : content,
246
+ );
247
+ return [
248
+ ...observedEntries(
249
+ ledger?.terminology,
250
+ 'terminology',
251
+ ({ term }) => term,
252
+ ({ term }) => term,
253
+ text,
254
+ ),
255
+ ...observedEntries(
256
+ ledger?.statuses,
257
+ 'statuses',
258
+ ({ subject }) => subject,
259
+ ({ value }) => value,
260
+ text,
261
+ ),
262
+ ...observedEntries(
263
+ ledger?.numbers,
264
+ 'numericClaims',
265
+ ({ subject }) => subject,
266
+ ({ value }) => value,
267
+ text,
268
+ true,
269
+ ),
270
+ ].map((observation) => ({
271
+ artifactId,
272
+ contentHash,
273
+ ...observation,
274
+ }));
275
+ }
276
+
277
+ function observedClaims(entries, keyOf, valueOf, text, numeric = false) {
278
+ return Object.fromEntries(
279
+ (entries ?? [])
280
+ .filter((entry) => claimObserved(text, valueOf(entry), numeric))
281
+ .map((entry) => [keyOf(entry), valueOf(entry)]),
282
+ );
283
+ }
284
+
285
+ function observedEntries(
286
+ entries,
287
+ group,
288
+ keyOf,
289
+ valueOf,
290
+ text,
291
+ numeric = false,
292
+ ) {
293
+ return (entries ?? [])
294
+ .filter((entry) => claimObserved(text, valueOf(entry), numeric))
295
+ .map((entry) => ({
296
+ group,
297
+ claim: keyOf(entry),
298
+ value: valueOf(entry),
299
+ }));
300
+ }
301
+
302
+ function claimObserved(text, value, numeric) {
303
+ const normalized = String(value)
304
+ .replaceAll(/\s+/g, ' ')
305
+ .trim()
306
+ .toLocaleLowerCase();
307
+ if (!normalized) return false;
308
+ const escaped = normalized.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
309
+ const startsWithDigit = numeric && /^\p{N}/u.test(normalized);
310
+ const endsWithDigit = numeric && /\p{N}$/u.test(normalized);
311
+ const leftBoundary = startsWithDigit
312
+ ? '[^\\p{L}\\p{N}.,]'
313
+ : '[^\\p{L}\\p{N}]';
314
+ const rightBoundary = endsWithDigit
315
+ ? '(?![\\p{L}\\p{N}]|[.,]\\p{N})'
316
+ : '(?![\\p{L}\\p{N}])';
317
+ return new RegExp(
318
+ `(?:^|${leftBoundary})${escaped}${rightBoundary}`,
319
+ 'u',
320
+ ).test(text);
321
+ }
322
+
323
+ function visibleText(value) {
324
+ return String(value)
325
+ .replaceAll(/<[^>]+>/g, ' ')
326
+ .replaceAll(/\s+/g, ' ')
327
+ .trim()
328
+ .toLocaleLowerCase();
329
+ }
330
+
331
+ function formatErrors(errors) {
332
+ return errors
333
+ .slice(0, 3)
334
+ .map(({ code, message }) => `${code}: ${message}`)
335
+ .join('; ');
336
+ }
337
+
338
+ function visualReviewError(message) {
339
+ const error = new Error(message);
340
+ error.code = 'E_VISUAL_REVIEW';
341
+ return error;
342
+ }
343
+
344
+ async function captureSnapshot(runRoot, path) {
345
+ const absolutePath = confinedPath(runRoot, path);
346
+ const bytes = await readFile(absolutePath);
347
+ return { bytes, hash: hashBytes(bytes) };
348
+ }
349
+
350
+ async function assertSnapshotsUnchanged(runRoot, snapshots) {
351
+ for (const [path, snapshot] of snapshots) {
352
+ const current = await captureSnapshot(runRoot, path);
353
+ if (current.hash !== snapshot.hash) {
354
+ throw visualReviewError(
355
+ `Visual review evidence changed while the critic inspected ${path}.`,
356
+ );
357
+ }
358
+ }
359
+ }
360
+
361
+ function confinedPath(runRoot, path) {
362
+ const root = resolve(runRoot);
363
+ const absolutePath = resolve(root, path);
364
+ const relativePath = relative(root, absolutePath);
365
+ if (
366
+ relativePath === '' ||
367
+ relativePath === '..' ||
368
+ relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
369
+ isAbsolute(relativePath)
370
+ ) {
371
+ throw visualReviewError(
372
+ `Visual review evidence path is not confined: ${path}.`,
373
+ );
374
+ }
375
+ return absolutePath;
376
+ }
377
+
378
+ function hashBytes(bytes) {
379
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
380
+ }
@@ -60,6 +60,8 @@ export async function runRenderQaStage({
60
60
  artifacts,
61
61
  browserProbe,
62
62
  widths,
63
+ evidenceRoot,
64
+ requireBrowserEvidence,
63
65
  } = {}) {
64
66
  assertStageInput(siteDir, artifacts);
65
67
  // Render QA is opt-in: the stage drives a probe the caller supplies and never
@@ -79,10 +81,19 @@ export async function runRenderQaStage({
79
81
  artifacts,
80
82
  probe: browserProbe,
81
83
  widths,
84
+ evidenceRoot,
85
+ requireBrowserEvidence,
82
86
  });
83
87
  }
84
88
 
85
- async function probeSiteArtifacts({ siteDir, artifacts, probe, widths }) {
89
+ async function probeSiteArtifacts({
90
+ siteDir,
91
+ artifacts,
92
+ probe,
93
+ widths,
94
+ evidenceRoot,
95
+ requireBrowserEvidence,
96
+ }) {
86
97
  return withSiteServer(siteDir, async (origin) => {
87
98
  const probeArtifacts = await Promise.all(
88
99
  artifacts.map(async (artifact) => {
@@ -105,6 +116,8 @@ async function probeSiteArtifacts({ siteDir, artifacts, probe, widths }) {
105
116
  artifacts: probeArtifacts,
106
117
  probe,
107
118
  ...(widths && { widths }),
119
+ ...(evidenceRoot && { evidenceRoot }),
120
+ ...(requireBrowserEvidence && { requireEvidence: true }),
108
121
  });
109
122
  return {
110
123
  valid: true,
@@ -112,6 +125,7 @@ async function probeSiteArtifacts({ siteDir, artifacts, probe, widths }) {
112
125
  warnings: renderQaWarningIds(browser.issues),
113
126
  issues: browser.issues,
114
127
  probes: browser.probes,
128
+ ...(browser.evidence && { evidence: browser.evidence }),
115
129
  };
116
130
  });
117
131
  }
@@ -176,16 +190,30 @@ export function selectReleaseVisualMatrix() {
176
190
  export async function runReleaseVisualMatrix({
177
191
  matrix = selectReleaseVisualMatrix(),
178
192
  browserProbe,
193
+ browserSession,
194
+ evidenceRoot,
195
+ onProbeResult,
179
196
  } = {}) {
180
197
  if (!Array.isArray(matrix) || matrix.length === 0) {
181
198
  throw new TypeError('Release visual QA requires a non-empty matrix.');
182
199
  }
183
- if (typeof browserProbe !== 'function') {
184
- throw new TypeError('Release visual QA requires a browser probe callback.');
200
+ if (browserSession !== undefined && browserProbe !== undefined) {
201
+ throw new TypeError(
202
+ 'Release visual QA accepts either a trusted browser session or a bare non-retaining probe, not both.',
203
+ );
204
+ }
205
+ if (browserSession === undefined && typeof browserProbe !== 'function') {
206
+ throw new TypeError(
207
+ 'Release visual QA requires a trusted browser session or browser probe callback.',
208
+ );
209
+ }
210
+ if (onProbeResult !== undefined && typeof onProbeResult !== 'function') {
211
+ throw new TypeError('Release visual QA probe observer must be a callback.');
185
212
  }
186
213
 
187
214
  const issues = [];
188
215
  const cases = [];
216
+ const evidence = [];
189
217
  for (const entry of matrix) {
190
218
  const { theme } = await resolveTheme({
191
219
  palette: entry.palette,
@@ -208,25 +236,30 @@ export async function runReleaseVisualMatrix({
208
236
  },
209
237
  ],
210
238
  widths: entry.viewports,
211
- browserProbe: async (request) => {
212
- const result = await browserProbe(request);
239
+ ...(browserSession === undefined ? { browserProbe } : { browserSession }),
240
+ onProbeResult: async (observation) => {
213
241
  if (
214
242
  entry.artifact.type === 'deck' &&
215
- request.scenario === 'default' &&
216
- result?.deckLayout?.flow !== 'horizontal'
243
+ observation.scenario === 'default' &&
244
+ observation.result?.deckLayout?.flow !== 'horizontal'
217
245
  ) {
218
246
  issues.push({
219
247
  artifactId: entry.id,
220
- width: request.viewport.width,
248
+ width: observation.viewport.width,
221
249
  scenario: 'default',
222
250
  code: 'deck-horizontal-layout',
223
251
  message: 'Interactive deck must page horizontally by default.',
224
252
  });
225
253
  }
226
- return result;
254
+ await onProbeResult?.(observation);
227
255
  },
256
+ ...(evidenceRoot && {
257
+ evidenceRoot,
258
+ requireBrowserEvidence: true,
259
+ }),
228
260
  });
229
261
  issues.push(...report.issues);
262
+ evidence.push(...(report.browser?.evidence ?? []));
230
263
  cases.push({
231
264
  id: entry.id,
232
265
  artifactType: entry.artifact.type,
@@ -238,7 +271,12 @@ export async function runReleaseVisualMatrix({
238
271
  });
239
272
  }
240
273
 
241
- return { valid: issues.length === 0, issues, cases };
274
+ return {
275
+ valid: issues.length === 0,
276
+ issues,
277
+ cases,
278
+ ...(evidenceRoot && { evidence }),
279
+ };
242
280
  }
243
281
 
244
282
  export async function runRenderQaCli(