@open-agent-toolkit/cli 0.2.26 → 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.
- package/assets/NOTICES.md +156 -0
- package/assets/docs/contributing/explainer-kit-verification.md +125 -0
- package/assets/docs/contributing/index.md +1 -0
- package/assets/docs/reference/troubleshooting.md +47 -0
- package/assets/docs/workflows/projects/artifacts.md +24 -6
- package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
- package/assets/docs/workflows/skills/explainer-kit.md +121 -69
- package/assets/docs/workflows/skills/index.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +18 -3
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
- package/assets/skills/explainer-kit/references/contracts.md +167 -20
- package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
- package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
- package/assets/skills/explainer-kit/references/visual-review.md +57 -0
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
- package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
- package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
- package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
- package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
- package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
- package/dist/commands/project/archive/archive-utils.d.ts +1 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +109 -42
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
- package/dist/commands/project/archive/push-runner.d.ts +2 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +5 -1
- package/dist/release/public-package-contract.d.ts +6 -0
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +75 -0
- 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({
|
|
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 (
|
|
184
|
-
throw new TypeError(
|
|
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:
|
|
212
|
-
|
|
239
|
+
...(browserSession === undefined ? { browserProbe } : { browserSession }),
|
|
240
|
+
onProbeResult: async (observation) => {
|
|
213
241
|
if (
|
|
214
242
|
entry.artifact.type === 'deck' &&
|
|
215
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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(
|