harnery 0.37.0 → 0.38.0
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/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
package/dist/commands/browse.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { PNG } from "pngjs";
|
|
4
5
|
import { resolveBinName } from "../core/config.js";
|
|
6
|
+
import { CAPTURE_FIDELITY_MISMATCH_THRESHOLD, compareBand, decideFidelity, pngDimensions, } from "../lib/browser/capture-fidelity.js";
|
|
5
7
|
import { Browser, browserProxyFromEnv, browserProxyGateFromEnv, captureDevOverlay, DEFAULT_CRITIQUE_RUBRIC, extractObservedIp, parseAssertSpec, runCritique, tilesFromFullPage, WEBRTC_PROXY_ONLY_ARG, writeNetscapeCookieFile, wslHeadedLaunchArgs, } from "../lib/browser/index.js";
|
|
8
|
+
import { findPackTile, readPackContext, tileId, writePackContext, writePackExpandedTile, } from "../lib/browser/page-review-pack.js";
|
|
6
9
|
import { buildQaManifest, classifySignatures, } from "../lib/browser/qa-plan.js";
|
|
7
10
|
import { DEFAULT_REUSE_MISMATCH_RATIO, planCritiqueReuse, QA_CRITIQUE_CONTRACT_VERSION, rubricDigest, } from "../lib/browser/qa-reuse.js";
|
|
8
11
|
import { loadQaSnapshot, resolveQaBaseline, saveQaSnapshot } from "../lib/browser/qa-snapshot.js";
|
|
9
12
|
import { startBrowserSessionServer, } from "../lib/browser/session-control.js";
|
|
10
13
|
import { diffAgainstBaseline, saveBaseline, } from "../lib/browser/visual-diff.js";
|
|
11
|
-
import { CookieJar } from "../lib/cookies/index.js";
|
|
14
|
+
import { applyExtraCookies, CookieJar } from "../lib/cookies/index.js";
|
|
12
15
|
/**
|
|
13
16
|
* `harn browse <url>`: Playwright-backed page navigation with shared
|
|
14
17
|
* cookie jar + persistent profile + diagnostics capture for LLM
|
|
@@ -237,6 +240,23 @@ export function registerBrowseCommand(program, emitParam, context, binName = res
|
|
|
237
240
|
"full fresh review, never an error. Stats land under `qaReuse` in the JSON envelope.")
|
|
238
241
|
.option("--qa-reuse-threshold <ratio>", "Band-diff mismatch ratio at or below which a clean tile region is reused " +
|
|
239
242
|
`(default ${DEFAULT_REUSE_MISMATCH_RATIO}; lower it toward 0 for stricter reuse).`)
|
|
243
|
+
.option("--review-pack <dir>", "Capture this render into a page review pack at <dir> (contexts/<id>/: full-page " +
|
|
244
|
+
"screenshot, critique tiles as PNG files, serialized DOM, QA signature) and make no " +
|
|
245
|
+
"vision call. A judge stage (`review-pack judge`, or qa-run) reviews the tiles from disk " +
|
|
246
|
+
"after every browser has closed. Tiling knobs are the --check-critique-* flags.")
|
|
247
|
+
.option("--review-pack-context <id>", "Context id inside the pack (default <viewport>-<theme>-<state> from --viewport, " +
|
|
248
|
+
"--qa-theme, --qa-state).")
|
|
249
|
+
.option("--review-pack-scope <selector>", "Also tile one screenshot per element matching <selector> into the pack (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
250
|
+
.option("--no-review-pack-bands", "Skip full-page bands in the pack and keep only --review-pack-scope tiles (a scoped " +
|
|
251
|
+
"visual pass).")
|
|
252
|
+
.option("--review-pack-expand <tile-id>", "Instead of capturing a context, re-render ONE existing tile of --review-pack-context " +
|
|
253
|
+
"(e.g. T012) at --device-scale-factor and write it beside the original as " +
|
|
254
|
+
"tiles/<tile>@<dpr>x.png. Existing tiles are untouched.")
|
|
255
|
+
.option("--review-pack-hit-rect <x,y,w,h>", "Document-space rectangle (integer px) a deterministic gate already flagged. Every " +
|
|
256
|
+
"full-page band that contains it is captured even past --check-critique-max-tiles, " +
|
|
257
|
+
`labelled \`hit band N\` (repeatable, at most ${REVIEW_PACK_HIT_RECT_MAX}).`, (value, previous = []) => [...previous, value], [])
|
|
258
|
+
.option("--device-scale-factor <n>", "Device scale factor for the browser context (default 1). 2 renders every screenshot " +
|
|
259
|
+
"at twice the pixels; pair with --review-pack-expand for a sharper look at one tile.")
|
|
240
260
|
.option("--no-dev-overlay", "Skip auto-capture of Next.js dev-overlay issues. Default: capture every queued error (kind/code/message/stack) when a <nextjs-portal> shadow root is present. Necessary because Next.js 16 + React 19 route hydration errors + most React warnings through onCaughtError → next-devtools' errorQueue, NOT through console.error, so Playwright's standard listener doesn't see them. Surfaces them in the JSON envelope under `devOverlay`.")
|
|
241
261
|
.action(async (url, opts) => {
|
|
242
262
|
try {
|
|
@@ -266,8 +286,18 @@ async function runBrowse(url, opts, context) {
|
|
|
266
286
|
const jar = opts.cookies === false
|
|
267
287
|
? null
|
|
268
288
|
: new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
|
|
289
|
+
if (jar)
|
|
290
|
+
applyExtraCookies(url, jar, context?.extraCookies);
|
|
269
291
|
const headed = opts.login || opts.headed;
|
|
270
292
|
const viewport = parseViewport(opts.viewport ?? "desktop");
|
|
293
|
+
const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
|
|
294
|
+
if (opts.reviewPackExpand !== undefined) {
|
|
295
|
+
if (opts.reviewPack === undefined || opts.reviewPackContext === undefined) {
|
|
296
|
+
throw new Error("--review-pack-expand requires --review-pack <dir> and --review-pack-context <id>.");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Malformed hit rects fail here, before a browser launches.
|
|
300
|
+
parseReviewPackHitRects(opts.reviewPackHitRect);
|
|
271
301
|
let colorScheme;
|
|
272
302
|
if (opts.colorScheme !== undefined) {
|
|
273
303
|
if (opts.colorScheme !== "light" && opts.colorScheme !== "dark") {
|
|
@@ -292,6 +322,7 @@ async function runBrowse(url, opts, context) {
|
|
|
292
322
|
headed,
|
|
293
323
|
jar,
|
|
294
324
|
viewport,
|
|
325
|
+
...(deviceScaleFactor !== undefined ? { deviceScaleFactor } : {}),
|
|
295
326
|
...(colorScheme ? { colorScheme } : {}),
|
|
296
327
|
navigationTimeout: Number.parseInt(opts.timeout, 10),
|
|
297
328
|
waitUntil: opts.waitUntil,
|
|
@@ -493,12 +524,26 @@ async function runBrowse(url, opts, context) {
|
|
|
493
524
|
emit.log(`qa-reuse: ${qaReuse.tiles_reused}/${qaReuse.tiles_total} tiles reused (band-diff), ` +
|
|
494
525
|
`${qaReuse.tiles_reviewed} reviewed fresh${qaReuse.invalidation ? ` — ${qaReuse.invalidation}` : ""}`, "info");
|
|
495
526
|
}
|
|
496
|
-
critique =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
527
|
+
critique = {
|
|
528
|
+
...(await runCritique({
|
|
529
|
+
url: navResult.url,
|
|
530
|
+
rubric,
|
|
531
|
+
tiles: tilesToReview,
|
|
532
|
+
provider: critiqueProvider,
|
|
533
|
+
})),
|
|
534
|
+
// Coverage rides on every envelope, skipped or not, so a reader can
|
|
535
|
+
// tell a pass over the whole page from a pass over its first N bands.
|
|
536
|
+
coverage: captured.coverage,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// Page review pack capture: tiles, DOM, and signature to disk, no vision
|
|
540
|
+
// call. Also before annotations, for the same reason as the critique.
|
|
541
|
+
let reviewPack;
|
|
542
|
+
if (opts.reviewPack !== undefined && opts.reviewPackExpand !== undefined) {
|
|
543
|
+
reviewPack = await expandReviewPackTile(browser, opts, deviceScaleFactor ?? 1);
|
|
544
|
+
}
|
|
545
|
+
else if (opts.reviewPack !== undefined) {
|
|
546
|
+
reviewPack = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
|
|
502
547
|
}
|
|
503
548
|
// Persist the QA baseline AFTER critique so a passing run's verdicts ride
|
|
504
549
|
// along with the snapshot (still before annotations mutate the page).
|
|
@@ -510,6 +555,7 @@ async function runBrowse(url, opts, context) {
|
|
|
510
555
|
const fullCoverage = critique?.provider &&
|
|
511
556
|
critique.outcome !== "skipped" &&
|
|
512
557
|
critiqueTiles !== undefined &&
|
|
558
|
+
!critique.coverage?.capped &&
|
|
513
559
|
(!qaReuse || qaReuse.tiles_reused === 0);
|
|
514
560
|
const persistedCritique = fullCoverage && critique && critiqueTiles
|
|
515
561
|
? {
|
|
@@ -631,10 +677,10 @@ async function runBrowse(url, opts, context) {
|
|
|
631
677
|
if (opts.baseline || opts.diff) {
|
|
632
678
|
throw new Error("--baseline / --diff require trio mode (screenshot file). Remove --snapshot/--html/--json or capture the screenshot first.");
|
|
633
679
|
}
|
|
634
|
-
await runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse ? summarizeReuse(qaReuse) : undefined);
|
|
680
|
+
await runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse ? summarizeReuse(qaReuse) : undefined, reviewPack);
|
|
635
681
|
}
|
|
636
682
|
else {
|
|
637
|
-
await runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse ? summarizeReuse(qaReuse) : undefined);
|
|
683
|
+
await runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse ? summarizeReuse(qaReuse) : undefined, reviewPack);
|
|
638
684
|
}
|
|
639
685
|
if (visibility && opts.checkVisibleAnnotate !== false) {
|
|
640
686
|
await browser.clearVisibilityAnnotations();
|
|
@@ -832,7 +878,7 @@ function splitBatchSteps(input) {
|
|
|
832
878
|
// ---------------------------------------------------------------------------
|
|
833
879
|
// Print mode (--snapshot / --html / --json): stdout, no file writes
|
|
834
880
|
// ---------------------------------------------------------------------------
|
|
835
|
-
async function runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse) {
|
|
881
|
+
async function runPrintMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse, reviewPack) {
|
|
836
882
|
let body = null;
|
|
837
883
|
if (opts.html) {
|
|
838
884
|
body = await browser.htmlContent(opts.selector);
|
|
@@ -882,6 +928,8 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
|
|
|
882
928
|
result.qaPlan = qaPlan;
|
|
883
929
|
if (qaReuse)
|
|
884
930
|
result.qaReuse = qaReuse;
|
|
931
|
+
if (reviewPack)
|
|
932
|
+
result.reviewPack = reviewPack;
|
|
885
933
|
if (batchResult && batchResult.clipboardReads.length > 0) {
|
|
886
934
|
result.batchClipboardReads = batchResult.clipboardReads;
|
|
887
935
|
}
|
|
@@ -903,7 +951,7 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
|
|
|
903
951
|
// ---------------------------------------------------------------------------
|
|
904
952
|
// Trio mode (default): write <prefix>.{png,html,json}
|
|
905
953
|
// ---------------------------------------------------------------------------
|
|
906
|
-
async function runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse) {
|
|
954
|
+
async function runTrioMode(browser, navResult, opts, evalResult, visibility, widths, overflow, runts, layoutLint, hit, content, critique, asserts, devOverlay, batchResult, qaPlan, qaReuse, reviewPack) {
|
|
907
955
|
const prefix = resolveOutPrefix(opts.out);
|
|
908
956
|
mkdirSync(dirname(prefix), { recursive: true });
|
|
909
957
|
const written = [];
|
|
@@ -1013,6 +1061,8 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
1013
1061
|
envelope.qaPlan = qaPlan;
|
|
1014
1062
|
if (qaReuse)
|
|
1015
1063
|
envelope.qaReuse = qaReuse;
|
|
1064
|
+
if (reviewPack)
|
|
1065
|
+
envelope.reviewPack = reviewPack;
|
|
1016
1066
|
if (batchResult && batchResult.clipboardReads.length > 0) {
|
|
1017
1067
|
envelope.batchClipboardReads = batchResult.clipboardReads;
|
|
1018
1068
|
}
|
|
@@ -1388,7 +1438,9 @@ function summarizeReuse(plan) {
|
|
|
1388
1438
|
* supplies the live-page inputs (signature, page height, explicit scope match
|
|
1389
1439
|
* counts). Classification and manifest math live in lib/browser/qa-plan.ts.
|
|
1390
1440
|
*/
|
|
1391
|
-
|
|
1441
|
+
/** Capture the live page's QA signature and serialized DOM for the store key
|
|
1442
|
+
* `--qa-target` (default: the url argument) and the labelled context. */
|
|
1443
|
+
async function captureQaState(browser, targetArg, renderedUrl, opts) {
|
|
1392
1444
|
const context = {
|
|
1393
1445
|
viewport: opts.viewport ?? "desktop",
|
|
1394
1446
|
theme: opts.qaTheme === "dark" ? "dark" : "light",
|
|
@@ -1403,11 +1455,16 @@ async function runQaPlanning(browser, targetArg, renderedUrl, opts) {
|
|
|
1403
1455
|
stylesheets: captured.stylesheets,
|
|
1404
1456
|
...(captured.truncated ? { truncated: true } : {}),
|
|
1405
1457
|
};
|
|
1458
|
+
return { target, context, signature, domHtml: captured.domHtml };
|
|
1459
|
+
}
|
|
1460
|
+
async function runQaPlanning(browser, targetArg, renderedUrl, opts) {
|
|
1461
|
+
const capture = await captureQaState(browser, targetArg, renderedUrl, opts);
|
|
1462
|
+
const { target, context, signature } = capture;
|
|
1406
1463
|
const report = {
|
|
1407
1464
|
signature: {
|
|
1408
|
-
nodes:
|
|
1409
|
-
stylesheets:
|
|
1410
|
-
truncated:
|
|
1465
|
+
nodes: signature.nodes.length,
|
|
1466
|
+
stylesheets: signature.stylesheets.length,
|
|
1467
|
+
truncated: Boolean(signature.truncated),
|
|
1411
1468
|
},
|
|
1412
1469
|
};
|
|
1413
1470
|
if (opts.qaPlan) {
|
|
@@ -1435,15 +1492,256 @@ async function runQaPlanning(browser, targetArg, renderedUrl, opts) {
|
|
|
1435
1492
|
estimatedFullPageTiles,
|
|
1436
1493
|
});
|
|
1437
1494
|
}
|
|
1438
|
-
return { report, capture
|
|
1495
|
+
return { report, capture };
|
|
1439
1496
|
}
|
|
1440
|
-
|
|
1497
|
+
/** Tiling knobs shared by the critique and the review-pack capture. The
|
|
1498
|
+
* routed provider's vision long-edge budget caps band height so tiles are
|
|
1499
|
+
* never silently downscaled by the model. */
|
|
1500
|
+
function critiqueTilingKnobs(opts, tileBudgetPx) {
|
|
1441
1501
|
const maxTiles = Math.max(1, Number.parseInt(opts.checkCritiqueMaxTiles ?? "24", 10));
|
|
1442
|
-
// The routed provider's vision long-edge budget caps band height so tiles
|
|
1443
|
-
// are never silently downscaled by the model.
|
|
1444
1502
|
const bandFlag = Math.max(200, Number.parseInt(opts.checkCritiqueBand ?? "1400", 10));
|
|
1445
1503
|
const band = tileBudgetPx ? Math.max(200, Math.min(bandFlag, tileBudgetPx)) : bandFlag;
|
|
1446
1504
|
const overlap = Math.max(0, Number.parseInt(opts.checkCritiqueOverlap ?? "120", 10));
|
|
1505
|
+
return { maxTiles, band, overlap };
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* `--review-pack-expand`: re-render one existing tile of a pack context at
|
|
1509
|
+
* the context's device scale factor. The tile's CSS-pixel rect is captured by
|
|
1510
|
+
* scrolling the viewport to it and clipping (`captureRegionByScroll`), which
|
|
1511
|
+
* renders the region the way a reader sees it and yields tile size × DPR
|
|
1512
|
+
* pixels. A second full-page screenshot is not taken: it would reproduce the
|
|
1513
|
+
* very raster artifact the expand is meant to check. No other pack file
|
|
1514
|
+
* changes.
|
|
1515
|
+
*/
|
|
1516
|
+
async function expandReviewPackTile(browser, opts, dpr) {
|
|
1517
|
+
const packDir = opts.reviewPack;
|
|
1518
|
+
const contextId = opts.reviewPackContext;
|
|
1519
|
+
const tileId = opts.reviewPackExpand;
|
|
1520
|
+
const { tile } = findPackTile([readPackContext(packDir, contextId)], tileId, contextId);
|
|
1521
|
+
const region = await browser.captureRegionByScroll({
|
|
1522
|
+
x: tile.x,
|
|
1523
|
+
y: tile.scrollY,
|
|
1524
|
+
width: tile.width,
|
|
1525
|
+
height: tile.height,
|
|
1526
|
+
});
|
|
1527
|
+
const { record, expanded } = writePackExpandedTile(packDir, contextId, {
|
|
1528
|
+
tileId,
|
|
1529
|
+
dpr,
|
|
1530
|
+
region,
|
|
1531
|
+
});
|
|
1532
|
+
emit.log(`review-pack: ${record.id}/${tileId} expanded at ${dpr}× → ${expanded.width}×${expanded.height} px, ${expanded.file}`, "info");
|
|
1533
|
+
return {
|
|
1534
|
+
dir: packDir,
|
|
1535
|
+
context_id: record.id,
|
|
1536
|
+
tiles: record.tiles.length,
|
|
1537
|
+
scopes: record.scopes,
|
|
1538
|
+
coverage: record.coverage,
|
|
1539
|
+
files: record.files,
|
|
1540
|
+
expanded,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Capture-fidelity probe (capture-fidelity.ts). Every band is re-shot by
|
|
1545
|
+
* scrolling the viewport (`Browser.captureRegionByScroll`) and pixel compared
|
|
1546
|
+
* with the crop the tiler took from the full-page screenshot. When they all
|
|
1547
|
+
* agree the tiles stand and the record says `full-page`; when any disagrees,
|
|
1548
|
+
* every band is re-cut from its scrolled capture (same rects, same ids, only
|
|
1549
|
+
* the pixels change) and the record says `scrolled-bands`. A probe that
|
|
1550
|
+
* cannot run, or a re-capture that comes back the wrong size (a page wider
|
|
1551
|
+
* than the viewport), keeps the full-page crop for that band and says so in a
|
|
1552
|
+
* warning. Runs while the capture browser is open; the judge never opens one.
|
|
1553
|
+
*/
|
|
1554
|
+
async function reconcileCaptureFidelity(browser, tiles) {
|
|
1555
|
+
const threshold = CAPTURE_FIDELITY_MISMATCH_THRESHOLD;
|
|
1556
|
+
const warnings = [];
|
|
1557
|
+
const dpr = await browser.currentPage.evaluate(() => window.devicePixelRatio || 1);
|
|
1558
|
+
// Tile rects live in the full-page PNG's pixel space; the scroll capture
|
|
1559
|
+
// takes CSS px and renders at the page's DPR, so the two line up again.
|
|
1560
|
+
const cssRect = (tile) => ({
|
|
1561
|
+
x: (tile.x ?? 0) / dpr,
|
|
1562
|
+
y: tile.scrollY / dpr,
|
|
1563
|
+
width: tile.width / dpr,
|
|
1564
|
+
height: tile.height / dpr,
|
|
1565
|
+
});
|
|
1566
|
+
const shots = new Map();
|
|
1567
|
+
const shoot = async (position) => {
|
|
1568
|
+
if (shots.has(position))
|
|
1569
|
+
return shots.get(position);
|
|
1570
|
+
const tile = tiles[position];
|
|
1571
|
+
let shot;
|
|
1572
|
+
try {
|
|
1573
|
+
const captured = await browser.captureRegionByScroll(cssRect(tile));
|
|
1574
|
+
const size = pngDimensions(captured);
|
|
1575
|
+
if (Math.abs(size.width - tile.width) > 1 || Math.abs(size.height - tile.height) > 1) {
|
|
1576
|
+
warnings.push(`review-pack: scrolled capture of ${tileId(position)} came back ` +
|
|
1577
|
+
`${size.width}×${size.height} px for a ${tile.width}×${tile.height} px band; ` +
|
|
1578
|
+
"its full-page crop was kept");
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
shot = captured;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
catch (err) {
|
|
1585
|
+
warnings.push(`review-pack: scrolled capture of ${tileId(position)} failed ` +
|
|
1586
|
+
`(${err instanceof Error ? err.message : String(err)}); its full-page crop was kept`);
|
|
1587
|
+
}
|
|
1588
|
+
shots.set(position, shot);
|
|
1589
|
+
return shot;
|
|
1590
|
+
};
|
|
1591
|
+
// Every band is probed. A two-band sample (top and middle) missed a raster
|
|
1592
|
+
// artifact that sat elsewhere on the page, and once every band has been
|
|
1593
|
+
// shot by scrolling, the fallback below costs nothing extra: the same shots
|
|
1594
|
+
// become the tiles.
|
|
1595
|
+
const probes = [];
|
|
1596
|
+
for (let position = 0; position < tiles.length; position++) {
|
|
1597
|
+
const tile = tiles[position];
|
|
1598
|
+
const shot = await shoot(position);
|
|
1599
|
+
if (!shot)
|
|
1600
|
+
continue;
|
|
1601
|
+
const cmp = compareBand(PNG.sync.read(Buffer.from(tile.pngBase64, "base64")), PNG.sync.read(shot), threshold);
|
|
1602
|
+
probes.push({
|
|
1603
|
+
tile_id: tileId(position),
|
|
1604
|
+
scrollY: tile.scrollY,
|
|
1605
|
+
height: tile.height,
|
|
1606
|
+
mismatch_ratio: cmp.mismatch_ratio,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const fidelity = decideFidelity(probes, threshold);
|
|
1610
|
+
if (fidelity.source === "full-page")
|
|
1611
|
+
return { tiles, fidelity, warnings };
|
|
1612
|
+
const recut = [];
|
|
1613
|
+
for (let position = 0; position < tiles.length; position++) {
|
|
1614
|
+
const tile = tiles[position];
|
|
1615
|
+
const shot = await shoot(position);
|
|
1616
|
+
recut.push(shot ? { ...tile, pngBase64: shot.toString("base64") } : tile);
|
|
1617
|
+
}
|
|
1618
|
+
warnings.unshift(`review-pack: the full-page screenshot disagreed with the viewport render on ` +
|
|
1619
|
+
`${fidelity.mismatched.join(", ")} (mismatch ratio above ${threshold}); every band was ` +
|
|
1620
|
+
"re-captured by scrolling the viewport");
|
|
1621
|
+
return { tiles: recut, fidelity, warnings };
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Capture this render into a page review pack: one full-page screenshot,
|
|
1625
|
+
* banded tiles (unless --no-review-pack-bands), one tile set per
|
|
1626
|
+
* --review-pack-scope selector, the serialized DOM, and the QA signature —
|
|
1627
|
+
* all written to disk, with no vision call. The judge stage reads them back
|
|
1628
|
+
* once the browser is gone.
|
|
1629
|
+
*/
|
|
1630
|
+
async function captureReviewPackContext(browser, targetArg, navResult, opts, qaCapture) {
|
|
1631
|
+
const packDir = opts.reviewPack;
|
|
1632
|
+
const { maxTiles, band, overlap } = critiqueTilingKnobs(opts);
|
|
1633
|
+
const buffer = await browser.fullPageScreenshotBuffer();
|
|
1634
|
+
let atoms;
|
|
1635
|
+
try {
|
|
1636
|
+
atoms = await browser.visualAtoms();
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
atoms = undefined;
|
|
1640
|
+
}
|
|
1641
|
+
const tiling = { bandHeight: band, overlap, maxTiles, atoms };
|
|
1642
|
+
const withBands = opts.reviewPackBands !== false;
|
|
1643
|
+
// Gate-hit rectangles (qa-run lifts them from the gate stage, which runs
|
|
1644
|
+
// first) pull the bands they land in past the cap; those tiles follow the
|
|
1645
|
+
// kept bands as `hit band N`. Coverage still describes the kept prefix.
|
|
1646
|
+
const hitRects = parseReviewPackHitRects(opts.reviewPackHitRect);
|
|
1647
|
+
const banded = withBands
|
|
1648
|
+
? tilesFromFullPage(buffer, { ...tiling, hitRects, maxHitBands: REVIEW_PACK_HIT_RECT_MAX })
|
|
1649
|
+
: undefined;
|
|
1650
|
+
if (banded?.coverage.capped) {
|
|
1651
|
+
emit.log(`review-pack: page banded to the ${maxTiles}-tile cap (raise --check-critique-max-tiles for full coverage)`, "warn");
|
|
1652
|
+
}
|
|
1653
|
+
if (banded && banded.hitBands > 0) {
|
|
1654
|
+
emit.log(`review-pack: ${banded.hitBands} band(s) below the tile cap captured for ${hitRects.length} gate-hit rect(s)`, "info");
|
|
1655
|
+
}
|
|
1656
|
+
// Fidelity: prove the full-page screenshot against scrolled viewport
|
|
1657
|
+
// renders while the browser is still open; on disagreement the bands are
|
|
1658
|
+
// re-cut from scrolled captures (see reconcileCaptureFidelity).
|
|
1659
|
+
let bandTiles = banded?.tiles ?? [];
|
|
1660
|
+
let captureFidelity;
|
|
1661
|
+
const fidelityWarnings = [];
|
|
1662
|
+
if (bandTiles.length > 0) {
|
|
1663
|
+
const reconciled = await reconcileCaptureFidelity(browser, bandTiles);
|
|
1664
|
+
bandTiles = reconciled.tiles;
|
|
1665
|
+
captureFidelity = reconciled.fidelity;
|
|
1666
|
+
fidelityWarnings.push(...reconciled.warnings);
|
|
1667
|
+
for (const warning of reconciled.warnings)
|
|
1668
|
+
emit.log(warning, "warn");
|
|
1669
|
+
emit.log(`review-pack: capture fidelity ${captureFidelity.source} ` +
|
|
1670
|
+
`(${captureFidelity.probed.length} band(s) probed, ${captureFidelity.mismatched.length} mismatched)`, "info");
|
|
1671
|
+
}
|
|
1672
|
+
const scopeTiles = [];
|
|
1673
|
+
for (const selector of opts.reviewPackScope ?? []) {
|
|
1674
|
+
const elementRects = await browser.elementTiles(selector);
|
|
1675
|
+
const scoped = tilesFromFullPage(buffer, { ...tiling, elementRects });
|
|
1676
|
+
if (elementRects.length > maxTiles) {
|
|
1677
|
+
emit.log(`review-pack: ${elementRects.length} elements matched ${selector}, capped to ${maxTiles} tiles`, "warn");
|
|
1678
|
+
}
|
|
1679
|
+
scopeTiles.push({ selector, tiles: scoped.tiles, coverage: scoped.coverage });
|
|
1680
|
+
}
|
|
1681
|
+
// Coverage: the bands when present, else the scopes folded together
|
|
1682
|
+
// (heights worst-case, band counts add, capped sticky).
|
|
1683
|
+
let coverage = banded?.coverage;
|
|
1684
|
+
if (!coverage) {
|
|
1685
|
+
for (const scope of scopeTiles) {
|
|
1686
|
+
coverage = coverage
|
|
1687
|
+
? {
|
|
1688
|
+
page_height_px: Math.max(coverage.page_height_px, scope.coverage.page_height_px),
|
|
1689
|
+
reviewed_height_px: Math.min(coverage.reviewed_height_px, scope.coverage.reviewed_height_px),
|
|
1690
|
+
bands_total: coverage.bands_total + scope.coverage.bands_total,
|
|
1691
|
+
bands_reviewed: coverage.bands_reviewed + scope.coverage.bands_reviewed,
|
|
1692
|
+
capped: coverage.capped || scope.coverage.capped,
|
|
1693
|
+
}
|
|
1694
|
+
: scope.coverage;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
const page = await browser.currentPage.evaluate(() => ({
|
|
1698
|
+
width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
1699
|
+
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
1700
|
+
}));
|
|
1701
|
+
const capture = qaCapture ?? (await captureQaState(browser, targetArg, navResult.url, opts));
|
|
1702
|
+
const record = writePackContext(packDir, {
|
|
1703
|
+
context: {
|
|
1704
|
+
...capture.context,
|
|
1705
|
+
...(opts.reviewPackContext ? { id: opts.reviewPackContext } : {}),
|
|
1706
|
+
},
|
|
1707
|
+
url: navResult.url,
|
|
1708
|
+
title: navResult.title,
|
|
1709
|
+
fullPage: buffer,
|
|
1710
|
+
pageWidth: page.width,
|
|
1711
|
+
pageHeight: page.height,
|
|
1712
|
+
tiles: bandTiles,
|
|
1713
|
+
coverage: coverage ?? {
|
|
1714
|
+
page_height_px: page.height,
|
|
1715
|
+
reviewed_height_px: 0,
|
|
1716
|
+
bands_total: 0,
|
|
1717
|
+
bands_reviewed: 0,
|
|
1718
|
+
capped: false,
|
|
1719
|
+
},
|
|
1720
|
+
scopeTiles: scopeTiles.map(({ selector, tiles }) => ({ selector, tiles })),
|
|
1721
|
+
signature: capture.signature,
|
|
1722
|
+
domHtml: capture.domHtml,
|
|
1723
|
+
...(captureFidelity ? { captureFidelity } : {}),
|
|
1724
|
+
hitBands: banded?.hitBands ?? 0,
|
|
1725
|
+
});
|
|
1726
|
+
emit.log(`review-pack: ${record.id} → ${record.tiles.length} tile(s)` +
|
|
1727
|
+
(record.scopes.length > 0
|
|
1728
|
+
? ` (${record.scopes.map((s) => `${s.selector}: ${s.tiles}`).join(", ")})`
|
|
1729
|
+
: "") +
|
|
1730
|
+
` written to ${packDir}`, "info");
|
|
1731
|
+
return {
|
|
1732
|
+
dir: packDir,
|
|
1733
|
+
context_id: record.id,
|
|
1734
|
+
tiles: record.tiles.length,
|
|
1735
|
+
scopes: record.scopes,
|
|
1736
|
+
coverage: record.coverage,
|
|
1737
|
+
files: record.files,
|
|
1738
|
+
...(record.hit_bands !== undefined ? { hit_bands: record.hit_bands } : {}),
|
|
1739
|
+
...(record.capture_fidelity ? { capture_fidelity: record.capture_fidelity } : {}),
|
|
1740
|
+
...(fidelityWarnings.length > 0 ? { warnings: fidelityWarnings } : {}),
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
async function captureCritiqueTiles(browser, opts, tileBudgetPx) {
|
|
1744
|
+
const { maxTiles, band, overlap } = critiqueTilingKnobs(opts, tileBudgetPx);
|
|
1447
1745
|
// Capture the whole page ONCE, then crop tiles from the pixels. Playwright's
|
|
1448
1746
|
// clip is viewport-relative, so a below-fold band clip throws ("clipped area
|
|
1449
1747
|
// outside the resulting image") — cropping the full-page image sidesteps that
|
|
@@ -1462,7 +1760,7 @@ async function captureCritiqueTiles(browser, opts, tileBudgetPx) {
|
|
|
1462
1760
|
catch {
|
|
1463
1761
|
atoms = undefined;
|
|
1464
1762
|
}
|
|
1465
|
-
const tiles = tilesFromFullPage(buffer, {
|
|
1763
|
+
const { tiles, coverage } = tilesFromFullPage(buffer, {
|
|
1466
1764
|
elementRects,
|
|
1467
1765
|
bandHeight: band,
|
|
1468
1766
|
overlap,
|
|
@@ -1475,7 +1773,7 @@ async function captureCritiqueTiles(browser, opts, tileBudgetPx) {
|
|
|
1475
1773
|
else if (!elementRects && tiles.length >= maxTiles) {
|
|
1476
1774
|
emit.log(`check-critique: page banded to the ${maxTiles}-tile cap (raise --check-critique-max-tiles for full coverage)`, "warn");
|
|
1477
1775
|
}
|
|
1478
|
-
return { tiles, fullPage: buffer };
|
|
1776
|
+
return { tiles, fullPage: buffer, coverage };
|
|
1479
1777
|
}
|
|
1480
1778
|
function logCritiqueSummary(critique) {
|
|
1481
1779
|
if (critique.outcome === "skipped") {
|
|
@@ -1560,6 +1858,36 @@ async function waitForTerminalEnter(signal) {
|
|
|
1560
1858
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1561
1859
|
});
|
|
1562
1860
|
}
|
|
1861
|
+
/** Upper bound on --review-pack-hit-rect values (and on hit bands cut past the cap). */
|
|
1862
|
+
const REVIEW_PACK_HIT_RECT_MAX = 50;
|
|
1863
|
+
/** `x,y,w,h` integer rectangles from repeated --review-pack-hit-rect flags. */
|
|
1864
|
+
function parseReviewPackHitRects(specs) {
|
|
1865
|
+
const list = specs ?? [];
|
|
1866
|
+
if (list.length > REVIEW_PACK_HIT_RECT_MAX) {
|
|
1867
|
+
throw new Error(`--review-pack-hit-rect accepts at most ${REVIEW_PACK_HIT_RECT_MAX} rectangles (got ${list.length}).`);
|
|
1868
|
+
}
|
|
1869
|
+
return list.map((spec) => {
|
|
1870
|
+
const parts = spec.split(",").map((part) => part.trim());
|
|
1871
|
+
const nums = parts.map((part) => (/^-?\d+$/.test(part) ? Number.parseInt(part, 10) : NaN));
|
|
1872
|
+
if (parts.length !== 4 || nums.some((n) => !Number.isFinite(n))) {
|
|
1873
|
+
throw new Error(`--review-pack-hit-rect must be four integers x,y,w,h in document px (got: ${spec}).`);
|
|
1874
|
+
}
|
|
1875
|
+
const [x, y, width, height] = nums;
|
|
1876
|
+
if (width < 0 || height < 0) {
|
|
1877
|
+
throw new Error(`--review-pack-hit-rect width and height must not be negative (got: ${spec}).`);
|
|
1878
|
+
}
|
|
1879
|
+
return { x, y, width, height };
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
function parseDeviceScaleFactor(spec) {
|
|
1883
|
+
if (spec === undefined)
|
|
1884
|
+
return undefined;
|
|
1885
|
+
const value = Number.parseFloat(spec);
|
|
1886
|
+
if (!Number.isFinite(value) || value < 1 || value > 4) {
|
|
1887
|
+
throw new Error(`--device-scale-factor must be a number between 1 and 4 (got: ${spec})`);
|
|
1888
|
+
}
|
|
1889
|
+
return value;
|
|
1890
|
+
}
|
|
1563
1891
|
function parseViewport(spec) {
|
|
1564
1892
|
const preset = VIEWPORT_PRESETS[spec.toLowerCase()];
|
|
1565
1893
|
if (preset)
|
package/dist/commands/fetch.js
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.js";
|
|
3
|
+
import { type QaRunContext, type QaRunResult } from "../lib/browser/qa-run-contracts.js";
|
|
4
|
+
export declare const QA_RECORD_EVIDENCE_SCHEMA_VERSION: 1;
|
|
5
|
+
/** The evidence document is copied into the run directory beside the result:
|
|
6
|
+
* the run identity's job_digest is taken over it, so without the copy nothing
|
|
7
|
+
* downstream could re-derive the digest. qa-run writes `job.json` for the same
|
|
8
|
+
* reason; the manual name keeps the two from being confused. */
|
|
9
|
+
export declare const QA_RECORD_EVIDENCE_FILENAME = "manual-evidence.json";
|
|
10
|
+
/** argv marker for a hand-performed check. The contract types argv as a string
|
|
11
|
+
* array and consumers print it, so an empty array would render as a command
|
|
12
|
+
* that ran with no arguments. A single explicit marker reads as "no process
|
|
13
|
+
* executed" and can never be mistaken for a re-runnable command line. */
|
|
14
|
+
export declare const QA_RECORD_MANUAL_ARGV: readonly ["<manual>"];
|
|
15
|
+
export type QaManualOutcome = "passed" | "failed" | "unknown";
|
|
16
|
+
/** One check a human or agent performed by hand. */
|
|
17
|
+
export interface QaManualCheck {
|
|
18
|
+
/** Context the check was performed in; matches a `contexts[].id` when the
|
|
19
|
+
* document declares contexts. */
|
|
20
|
+
context_id: string;
|
|
21
|
+
/** Stable check identifier. Prefix with `manual:` by convention. */
|
|
22
|
+
check_id: string;
|
|
23
|
+
outcome: QaManualOutcome;
|
|
24
|
+
/** What the recorder observed. Carried into the result's `failures` field. */
|
|
25
|
+
notes?: string[];
|
|
26
|
+
/** Files that back the observation. Every path must exist on disk. */
|
|
27
|
+
artifacts?: {
|
|
28
|
+
png?: string;
|
|
29
|
+
html?: string;
|
|
30
|
+
json?: string;
|
|
31
|
+
};
|
|
32
|
+
/** Time the check took, when the recorder measured it. */
|
|
33
|
+
wall_time_ms?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface QaManualEvidence {
|
|
36
|
+
schema_version: typeof QA_RECORD_EVIDENCE_SCHEMA_VERSION;
|
|
37
|
+
/** Who performed the checks (agent name or operator identity). */
|
|
38
|
+
recorded_by: string;
|
|
39
|
+
/** Why the runner could not be used. Required: a hand record with no stated
|
|
40
|
+
* reason is an assertion, not evidence. */
|
|
41
|
+
reason: string;
|
|
42
|
+
checks: QaManualCheck[];
|
|
43
|
+
/** Declared contexts. Absent: derived from the checks' context IDs. */
|
|
44
|
+
contexts?: QaRunContext[];
|
|
45
|
+
}
|
|
46
|
+
export type QaManualEvidenceValidation = {
|
|
47
|
+
ok: true;
|
|
48
|
+
evidence: QaManualEvidence;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
errors: string[];
|
|
52
|
+
};
|
|
53
|
+
/** Artifact paths are resolved against the directory holding the evidence
|
|
54
|
+
* document, not the working directory: an evidence file names the captures
|
|
55
|
+
* sitting beside it, and that stays true wherever it is recorded from. */
|
|
56
|
+
export declare function resolveArtifactPath(baseDir: string, path: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Validate an untrusted evidence document. Every structural problem, every
|
|
59
|
+
* missing artifact, and every secret-bearing field is reported at once, before
|
|
60
|
+
* anything is written: a caller fixes the document in one pass, and a
|
|
61
|
+
* half-valid record never reaches disk.
|
|
62
|
+
*/
|
|
63
|
+
export declare function validateManualEvidence(value: unknown, options: {
|
|
64
|
+
baseDir: string;
|
|
65
|
+
}): QaManualEvidenceValidation;
|
|
66
|
+
/** Contexts the result reports: the document's own declarations when it has
|
|
67
|
+
* them, otherwise one derived per distinct check context ID. Derivation reads
|
|
68
|
+
* the runner's `<viewport>-<theme>-<state>` convention when the ID follows it
|
|
69
|
+
* and records `unknown` rather than guessing when it does not. */
|
|
70
|
+
export declare function deriveContexts(evidence: QaManualEvidence): QaRunContext[];
|
|
71
|
+
/** One git probe for the identity block, matching qa-run. Failure is not an
|
|
72
|
+
* error: work outside any repository legitimately records "unknown". */
|
|
73
|
+
export declare function probeRevision(): {
|
|
74
|
+
tested_revision?: string;
|
|
75
|
+
worktree_dirty?: boolean;
|
|
76
|
+
} | undefined;
|
|
77
|
+
export interface BuildManualResultInput {
|
|
78
|
+
evidence: QaManualEvidence;
|
|
79
|
+
target: string;
|
|
80
|
+
mode: "signoff" | "review";
|
|
81
|
+
/** Parent directory; the result lands in `<outParent>/run-<run_id>/`. */
|
|
82
|
+
outParent: string;
|
|
83
|
+
/** Pre-minted run ID (tests); default crypto.randomUUID. */
|
|
84
|
+
runId?: string;
|
|
85
|
+
/** Directory the artifact paths resolve against (the evidence file's dir). */
|
|
86
|
+
baseDir: string;
|
|
87
|
+
revisionProbe?: {
|
|
88
|
+
tested_revision?: string;
|
|
89
|
+
worktree_dirty?: boolean;
|
|
90
|
+
};
|
|
91
|
+
/** Completion instant (tests); default now. */
|
|
92
|
+
completedAt?: Date;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the QaRunResult for a validated evidence document. Pure apart from the
|
|
96
|
+
* host sample and the clock, so the shape is testable without touching disk.
|
|
97
|
+
*/
|
|
98
|
+
export declare function buildManualResult(input: BuildManualResultInput): QaRunResult;
|
|
99
|
+
export interface QaRecordWritePaths {
|
|
100
|
+
runDir: string;
|
|
101
|
+
resultPath: string;
|
|
102
|
+
evidencePath: string;
|
|
103
|
+
statusPath: string;
|
|
104
|
+
latestPath: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Write the run directory exactly the way the runner does: the result, the
|
|
108
|
+
* source document, a terminal status document, and the parent's latest.json
|
|
109
|
+
* pointer written temp-then-rename so a reader never sees a torn write.
|
|
110
|
+
*/
|
|
111
|
+
export declare function writeManualRun(result: QaRunResult, evidence: QaManualEvidence): QaRecordWritePaths;
|
|
112
|
+
export interface QaRecordOutcome {
|
|
113
|
+
/** 1 usage or validation error · 2 verdict failed · 4 verdict incomplete.
|
|
114
|
+
* Never 0: a hand-recorded result cannot report a pass. */
|
|
115
|
+
exit: 1 | 2 | 4;
|
|
116
|
+
result?: QaRunResult;
|
|
117
|
+
paths?: QaRecordWritePaths;
|
|
118
|
+
error?: string;
|
|
119
|
+
/** Individual validation errors, all of them, when the document was bad. */
|
|
120
|
+
evidenceErrors?: string[];
|
|
121
|
+
}
|
|
122
|
+
export interface QaRecordInput {
|
|
123
|
+
target: string;
|
|
124
|
+
evidencePath: string;
|
|
125
|
+
mode?: string;
|
|
126
|
+
outDir?: string;
|
|
127
|
+
/** Lazy default so invalid evidence does not create an empty artifact workspace. */
|
|
128
|
+
defaultOutDir?: () => string;
|
|
129
|
+
runId?: string;
|
|
130
|
+
completedAt?: Date;
|
|
131
|
+
/** Injectable for tests; default: the real git probe. */
|
|
132
|
+
revisionProbe?: () => {
|
|
133
|
+
tested_revision?: string;
|
|
134
|
+
worktree_dirty?: boolean;
|
|
135
|
+
} | undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Pure core of qa-record: read the evidence document, validate it completely,
|
|
139
|
+
* build the result, and write the run directory. The commander action is a
|
|
140
|
+
* thin wrapper over this so the whole path tests without a CLI harness.
|
|
141
|
+
*/
|
|
142
|
+
export declare function recordManualQa(input: QaRecordInput): QaRecordOutcome;
|
|
143
|
+
export declare function registerQaRecordCommand(program: Command, emit: EmitContext, context?: HarneryProgramContext): void;
|
|
144
|
+
//# sourceMappingURL=qa-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qa-record.d.ts","sourceRoot":"","sources":["../../src/commands/qa-record.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAQ1E,OAAO,EAQL,KAAK,YAAY,EAGjB,KAAK,WAAW,EAGjB,MAAM,oCAAoC,CAAC;AAE5C,eAAO,MAAM,iCAAiC,EAAG,CAAU,CAAC;AAE5D;;;gEAGgE;AAChE,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAElE;;;yEAGyE;AACzE,eAAO,MAAM,qBAAqB,uBAAwB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,oDAAoD;AACpD,MAAM,WAAW,aAAa;IAC5B;qCACiC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,SAAS,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,OAAO,iCAAiC,CAAC;IACzD,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB;+CAC2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,gBAAgB,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAIpC;;0EAE0E;AAC1E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzE;AAsBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3B,0BAA0B,CA4H5B;AAED;;;kEAGkE;AAClE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA0BzE;AAYD;wEACwE;AACxE,wBAAgB,aAAa,IACzB;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD,SAAS,CAcZ;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACvE,+CAA+C;IAC/C,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,WAAW,CA6G5E;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,GACzB,kBAAkB,CAiCpB;AAED,MAAM,WAAW,eAAe;IAC9B;+DAC2D;IAC3D,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAC1F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,CA+DpE;AASD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA+EN"}
|
|
Binary file
|