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.
Files changed (184) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -2
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +61 -1
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +48 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +349 -21
  17. package/dist/commands/fetch.js +1 -0
  18. package/dist/commands/qa-record.d.ts +144 -0
  19. package/dist/commands/qa-record.d.ts.map +1 -0
  20. package/dist/commands/qa-record.js +0 -0
  21. package/dist/commands/qa-run.d.ts +7 -3
  22. package/dist/commands/qa-run.d.ts.map +1 -1
  23. package/dist/commands/qa-run.js +268 -13
  24. package/dist/commands/qa-status.d.ts +71 -0
  25. package/dist/commands/qa-status.d.ts.map +1 -0
  26. package/dist/commands/qa-status.js +490 -0
  27. package/dist/commands/qa-verify.d.ts +40 -0
  28. package/dist/commands/qa-verify.d.ts.map +1 -0
  29. package/dist/commands/qa-verify.js +180 -0
  30. package/dist/commands/review-pack.d.ts +4 -0
  31. package/dist/commands/review-pack.d.ts.map +1 -0
  32. package/dist/commands/review-pack.js +1001 -0
  33. package/dist/core/agents/qa-signal.d.ts +111 -0
  34. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  35. package/dist/core/agents/qa-signal.js +231 -0
  36. package/dist/core/agents/session-name-display.d.ts +20 -5
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +67 -7
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  41. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  43. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  44. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  45. package/dist/core/agents/state/live-coordination-view.js +1 -0
  46. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  47. package/dist/core/artifacts/constants.d.ts +1 -1
  48. package/dist/core/artifacts/constants.js +1 -1
  49. package/dist/core/artifacts/index.d.ts +57 -6
  50. package/dist/core/artifacts/index.d.ts.map +1 -1
  51. package/dist/core/artifacts/index.js +265 -10
  52. package/dist/core/config.d.ts +8 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +16 -0
  55. package/dist/core/diagnostics/bundle.d.ts +16 -0
  56. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  57. package/dist/core/diagnostics/bundle.js +101 -7
  58. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  59. package/dist/core/events/v3/bootstrap.js +10 -0
  60. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  61. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  62. package/dist/core/events/v3/coordination-view.js +69 -8
  63. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  64. package/dist/core/events/v3/producers/intake.js +9 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts +23 -0
  66. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  67. package/dist/core/events/v3/producers/recorder.js +264 -18
  68. package/dist/core/hooks/cli.js +120 -27
  69. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  70. package/dist/core/hooks/resolve/transcript.js +10 -3
  71. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  72. package/dist/core/hooks/session-name-presence.js +4 -1
  73. package/dist/core/qa-artifacts.d.ts +20 -0
  74. package/dist/core/qa-artifacts.d.ts.map +1 -0
  75. package/dist/core/qa-artifacts.js +110 -0
  76. package/dist/core/resources/contract.d.ts +6 -0
  77. package/dist/core/resources/contract.d.ts.map +1 -1
  78. package/dist/core/resources/sampler.d.ts +7 -0
  79. package/dist/core/resources/sampler.d.ts.map +1 -1
  80. package/dist/core/resources/sampler.js +106 -5
  81. package/dist/lib/admission.d.ts +71 -0
  82. package/dist/lib/admission.d.ts.map +1 -0
  83. package/dist/lib/admission.js +264 -0
  84. package/dist/lib/agent-browser/client.d.ts +1 -1
  85. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  86. package/dist/lib/agent-browser/client.js +1 -5
  87. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  88. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  89. package/dist/lib/browser/capture-fidelity.js +84 -0
  90. package/dist/lib/browser/client.d.ts +41 -1
  91. package/dist/lib/browser/client.d.ts.map +1 -1
  92. package/dist/lib/browser/client.js +167 -9
  93. package/dist/lib/browser/critique.d.ts +38 -1
  94. package/dist/lib/browser/critique.d.ts.map +1 -1
  95. package/dist/lib/browser/critique.js +34 -6
  96. package/dist/lib/browser/index.d.ts +4 -2
  97. package/dist/lib/browser/index.d.ts.map +1 -1
  98. package/dist/lib/browser/index.js +2 -0
  99. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  100. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  101. package/dist/lib/browser/page-review-judge.js +270 -0
  102. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  103. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  104. package/dist/lib/browser/page-review-pack.js +1751 -0
  105. package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
  106. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  107. package/dist/lib/browser/qa-run-contracts.js +136 -1
  108. package/dist/lib/browser/qa-run.d.ts +100 -10
  109. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  110. package/dist/lib/browser/qa-run.js +768 -169
  111. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  112. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  113. package/dist/lib/browser/request-diagnostics.js +18 -0
  114. package/dist/lib/browser/tiling.d.ts +19 -0
  115. package/dist/lib/browser/tiling.d.ts.map +1 -1
  116. package/dist/lib/browser/tiling.js +28 -0
  117. package/dist/lib/cookies/client.d.ts +9 -0
  118. package/dist/lib/cookies/client.d.ts.map +1 -1
  119. package/dist/lib/cookies/client.js +197 -44
  120. package/dist/lib/cookies/extra.d.ts +18 -0
  121. package/dist/lib/cookies/extra.d.ts.map +1 -0
  122. package/dist/lib/cookies/extra.js +14 -0
  123. package/dist/lib/cookies/index.d.ts +2 -1
  124. package/dist/lib/cookies/index.d.ts.map +1 -1
  125. package/dist/lib/cookies/index.js +2 -1
  126. package/dist/lib/durable-job.d.ts +124 -0
  127. package/dist/lib/durable-job.d.ts.map +1 -0
  128. package/dist/lib/durable-job.js +296 -0
  129. package/dist/lib/http/client.d.ts +7 -1
  130. package/dist/lib/http/client.d.ts.map +1 -1
  131. package/dist/lib/http/client.js +2 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +5 -2
  134. package/package.json +8 -2
  135. package/src/commander.ts +50 -2
  136. package/src/commands/admission.ts +699 -0
  137. package/src/commands/agents.ts +87 -1
  138. package/src/commands/artifacts.ts +97 -21
  139. package/src/commands/browse-ai.ts +10 -5
  140. package/src/commands/browse.ts +481 -20
  141. package/src/commands/fetch.ts +1 -0
  142. package/src/commands/qa-record.ts +682 -0
  143. package/src/commands/qa-run.ts +335 -16
  144. package/src/commands/qa-status.ts +608 -0
  145. package/src/commands/qa-verify.ts +238 -0
  146. package/src/commands/review-pack.ts +1281 -0
  147. package/src/core/agents/qa-signal.ts +261 -0
  148. package/src/core/agents/session-name-display.ts +78 -7
  149. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  150. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  151. package/src/core/agents/state/live-coordination-view.ts +1 -0
  152. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  153. package/src/core/artifacts/constants.ts +1 -1
  154. package/src/core/artifacts/index.ts +370 -21
  155. package/src/core/config.ts +23 -0
  156. package/src/core/diagnostics/bundle.ts +119 -11
  157. package/src/core/events/v3/bootstrap.ts +10 -0
  158. package/src/core/events/v3/coordination-view.ts +100 -11
  159. package/src/core/events/v3/producers/intake.ts +9 -2
  160. package/src/core/events/v3/producers/recorder.ts +312 -18
  161. package/src/core/hooks/cli.ts +140 -32
  162. package/src/core/hooks/resolve/transcript.ts +10 -3
  163. package/src/core/hooks/session-name-presence.ts +6 -1
  164. package/src/core/qa-artifacts.ts +126 -0
  165. package/src/core/resources/contract.ts +7 -0
  166. package/src/core/resources/sampler.ts +138 -6
  167. package/src/lib/admission.ts +347 -0
  168. package/src/lib/agent-browser/client.ts +2 -10
  169. package/src/lib/browser/capture-fidelity.ts +98 -0
  170. package/src/lib/browser/client.ts +206 -10
  171. package/src/lib/browser/critique.ts +62 -7
  172. package/src/lib/browser/index.ts +36 -0
  173. package/src/lib/browser/page-review-judge.ts +360 -0
  174. package/src/lib/browser/page-review-pack.ts +2384 -0
  175. package/src/lib/browser/qa-run-contracts.ts +366 -3
  176. package/src/lib/browser/qa-run.ts +868 -190
  177. package/src/lib/browser/request-diagnostics.ts +27 -0
  178. package/src/lib/browser/tiling.ts +32 -0
  179. package/src/lib/cookies/client.ts +228 -42
  180. package/src/lib/cookies/extra.ts +28 -0
  181. package/src/lib/cookies/index.ts +2 -0
  182. package/src/lib/durable-job.ts +407 -0
  183. package/src/lib/http/client.ts +13 -1
  184. package/src/lib/instructions/templates.ts +5 -2
@@ -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 = await runCritique({
497
- url: navResult.url,
498
- rubric,
499
- tiles: tilesToReview,
500
- provider: critiqueProvider,
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
- async function runQaPlanning(browser, targetArg, renderedUrl, opts) {
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: captured.nodes.length,
1409
- stylesheets: captured.stylesheets.length,
1410
- truncated: captured.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: { target, context, signature, domHtml: captured.domHtml } };
1495
+ return { report, capture };
1439
1496
  }
1440
- async function captureCritiqueTiles(browser, opts, tileBudgetPx) {
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)
@@ -63,6 +63,7 @@ async function runFetch(url, opts, emit, context) {
63
63
  redirect: opts.redirect,
64
64
  signal: ac.signal,
65
65
  extraHeaders: context?.extraHeaders,
66
+ extraCookies: context?.extraCookies,
66
67
  responseType: "bytes",
67
68
  });
68
69
  }
@@ -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":"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"}
Binary file