harnery 0.38.0 → 0.40.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 (378) hide show
  1. package/dist/commands/agents.d.ts.map +1 -1
  2. package/dist/commands/agents.js +32 -8
  3. package/dist/commands/artifacts.d.ts.map +1 -1
  4. package/dist/commands/artifacts.js +97 -6
  5. package/dist/commands/backup.d.ts +16 -0
  6. package/dist/commands/backup.d.ts.map +1 -1
  7. package/dist/commands/backup.js +239 -33
  8. package/dist/commands/browse.d.ts +8 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +466 -231
  11. package/dist/commands/diagnostics.d.ts.map +1 -1
  12. package/dist/commands/diagnostics.js +6 -3
  13. package/dist/commands/ledger-v3.d.ts.map +1 -1
  14. package/dist/commands/ledger-v3.js +20 -4
  15. package/dist/commands/qa-run.d.ts.map +1 -1
  16. package/dist/commands/qa-run.js +26 -3
  17. package/dist/commands/qa-status.d.ts.map +1 -1
  18. package/dist/commands/qa-status.js +7 -2
  19. package/dist/commands/resources.d.ts.map +1 -1
  20. package/dist/commands/resources.js +23 -3
  21. package/dist/commands/review-pack.d.ts.map +1 -1
  22. package/dist/commands/review-pack.js +98 -23
  23. package/dist/commands/tunnel.d.ts.map +1 -1
  24. package/dist/commands/tunnel.js +2 -2
  25. package/dist/core/adapters/profiles.d.ts +1 -1
  26. package/dist/core/agents/canonical-emit.d.ts +3 -2
  27. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  28. package/dist/core/agents/canonical-emit.js +2 -0
  29. package/dist/core/agents/cli.js +13 -6
  30. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  31. package/dist/core/agents/live-authority-v3.js +5 -3
  32. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
  33. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  34. package/dist/core/agents/render/prompt-context.d.ts +7 -5
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +63 -8
  37. package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
  38. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +2 -1
  42. package/dist/core/agents/state/names.d.ts +35 -2
  43. package/dist/core/agents/state/names.d.ts.map +1 -1
  44. package/dist/core/agents/state/names.js +80 -5
  45. package/dist/core/agents/state/stale-sweep.d.ts +2 -1
  46. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  47. package/dist/core/agents/state/stale-sweep.js +23 -6
  48. package/dist/core/agents/turn-elapsed.d.ts +51 -0
  49. package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
  50. package/dist/core/agents/turn-elapsed.js +91 -0
  51. package/dist/core/artifacts/activity.d.ts +14 -0
  52. package/dist/core/artifacts/activity.d.ts.map +1 -0
  53. package/dist/core/artifacts/activity.js +52 -0
  54. package/dist/core/artifacts/delivery-card.d.ts +39 -0
  55. package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
  56. package/dist/core/artifacts/delivery-card.js +282 -0
  57. package/dist/core/artifacts/index.d.ts +42 -7
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +288 -76
  60. package/dist/core/artifacts/mutation-lock.d.ts +2 -0
  61. package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
  62. package/dist/core/artifacts/mutation-lock.js +91 -0
  63. package/dist/core/backup/host-snapshot.d.ts +18 -0
  64. package/dist/core/backup/host-snapshot.d.ts.map +1 -0
  65. package/dist/core/backup/host-snapshot.js +64 -0
  66. package/dist/core/config.d.ts +20 -5
  67. package/dist/core/config.d.ts.map +1 -1
  68. package/dist/core/config.js +44 -10
  69. package/dist/core/diagnostics/advice.d.ts +38 -3
  70. package/dist/core/diagnostics/advice.d.ts.map +1 -1
  71. package/dist/core/diagnostics/advice.js +103 -99
  72. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  73. package/dist/core/diagnostics/bundle.js +111 -47
  74. package/dist/core/diagnostics/comparison.d.ts.map +1 -1
  75. package/dist/core/diagnostics/comparison.js +3 -2
  76. package/dist/core/diagnostics/contract.d.ts +15 -31
  77. package/dist/core/diagnostics/contract.d.ts.map +1 -1
  78. package/dist/core/diagnostics/contract.js +2 -5
  79. package/dist/core/diagnostics/index.d.ts +2 -0
  80. package/dist/core/diagnostics/index.d.ts.map +1 -1
  81. package/dist/core/diagnostics/index.js +2 -0
  82. package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
  83. package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
  84. package/dist/core/diagnostics/pressure-contract.js +70 -0
  85. package/dist/core/diagnostics/pressure.d.ts +25 -0
  86. package/dist/core/diagnostics/pressure.d.ts.map +1 -0
  87. package/dist/core/diagnostics/pressure.js +1138 -0
  88. package/dist/core/diagnostics/replay.d.ts.map +1 -1
  89. package/dist/core/diagnostics/replay.js +90 -10
  90. package/dist/core/events/v3/adapter-id.d.ts +6 -0
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
  92. package/dist/core/events/v3/adapter-id.js +7 -0
  93. package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
  94. package/dist/core/events/v3/archive-retention.js +3 -2
  95. package/dist/core/events/v3/bootstrap.d.ts +41 -4
  96. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  97. package/dist/core/events/v3/bootstrap.js +120 -17
  98. package/dist/core/events/v3/capabilities.d.ts +16 -9
  99. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  100. package/dist/core/events/v3/capabilities.js +37 -2
  101. package/dist/core/events/v3/capability-drift.d.ts +2 -2
  102. package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.d.ts +48 -5
  104. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  105. package/dist/core/events/v3/control-witness.js +91 -36
  106. package/dist/core/events/v3/control.d.ts.map +1 -1
  107. package/dist/core/events/v3/control.js +22 -2
  108. package/dist/core/events/v3/index.d.ts +2 -1
  109. package/dist/core/events/v3/index.d.ts.map +1 -1
  110. package/dist/core/events/v3/index.js +2 -1
  111. package/dist/core/events/v3/live-feed.d.ts +7 -1
  112. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  113. package/dist/core/events/v3/live-feed.js +21 -3
  114. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  115. package/dist/core/events/v3/live-routing.js +10 -2
  116. package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
  117. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  118. package/dist/core/events/v3/producers/command-recorder.js +2 -1
  119. package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
  120. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  121. package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
  122. package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
  123. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  124. package/dist/core/events/v3/producers/hook-base.js +15 -5
  125. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  126. package/dist/core/events/v3/producers/hook.js +17 -4
  127. package/dist/core/events/v3/producers/intake.d.ts +4 -4
  128. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  129. package/dist/core/events/v3/producers/intake.js +3 -2
  130. package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
  131. package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
  132. package/dist/core/events/v3/producers/recorder.d.ts +16 -5
  133. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  134. package/dist/core/events/v3/producers/recorder.js +31 -9
  135. package/dist/core/events/v3/reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/reader.js +10 -13
  137. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  138. package/dist/core/events/v3/runtime-build.js +3 -2
  139. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
  140. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
  141. package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
  142. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  143. package/dist/core/hooks/adapter/output.d.ts +16 -14
  144. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  145. package/dist/core/hooks/adapter/output.js +36 -38
  146. package/dist/core/hooks/backup-schedule.d.ts +34 -0
  147. package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
  148. package/dist/core/hooks/backup-schedule.js +114 -0
  149. package/dist/core/hooks/cli.js +61 -18
  150. package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
  151. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
  152. package/dist/core/hooks/codex-wsl-bridge.js +18 -4
  153. package/dist/core/hooks/index.d.ts +1 -0
  154. package/dist/core/hooks/index.d.ts.map +1 -1
  155. package/dist/core/hooks/index.js +1 -0
  156. package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
  157. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
  158. package/dist/core/hooks/prompt-context/contract.js +102 -0
  159. package/dist/core/hooks/prompt-context/index.d.ts +5 -0
  160. package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
  161. package/dist/core/hooks/prompt-context/index.js +2 -0
  162. package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
  163. package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
  164. package/dist/core/hooks/prompt-context/runner.js +249 -0
  165. package/dist/core/qa-artifacts.d.ts +3 -0
  166. package/dist/core/qa-artifacts.d.ts.map +1 -1
  167. package/dist/core/qa-artifacts.js +25 -3
  168. package/dist/core/resources/contract.d.ts +69 -3
  169. package/dist/core/resources/contract.d.ts.map +1 -1
  170. package/dist/core/resources/contract.js +1 -1
  171. package/dist/core/resources/index.d.ts +1 -0
  172. package/dist/core/resources/index.d.ts.map +1 -1
  173. package/dist/core/resources/index.js +1 -0
  174. package/dist/core/resources/native.d.ts +36 -0
  175. package/dist/core/resources/native.d.ts.map +1 -0
  176. package/dist/core/resources/native.js +418 -0
  177. package/dist/core/resources/sampler.d.ts +2 -0
  178. package/dist/core/resources/sampler.d.ts.map +1 -1
  179. package/dist/core/resources/sampler.js +31 -1
  180. package/dist/core/resources/status.d.ts +61 -0
  181. package/dist/core/resources/status.d.ts.map +1 -0
  182. package/dist/core/resources/status.js +536 -0
  183. package/dist/core/resources/system.d.ts +27 -0
  184. package/dist/core/resources/system.d.ts.map +1 -0
  185. package/dist/core/resources/system.js +439 -0
  186. package/dist/core/semantic/contract.d.ts +13 -13
  187. package/dist/core/semantic/service.d.ts +2 -0
  188. package/dist/core/semantic/service.d.ts.map +1 -1
  189. package/dist/core/semantic/service.js +27 -4
  190. package/dist/core/storage/builtins.js +41 -0
  191. package/dist/core/supervisor/contract.d.ts +41 -3
  192. package/dist/core/supervisor/contract.d.ts.map +1 -1
  193. package/dist/core/supervisor/contract.js +3 -2
  194. package/dist/core/supervisor/findings.d.ts +3 -0
  195. package/dist/core/supervisor/findings.d.ts.map +1 -1
  196. package/dist/core/supervisor/findings.js +88 -0
  197. package/dist/core/supervisor/history.d.ts.map +1 -1
  198. package/dist/core/supervisor/history.js +14 -0
  199. package/dist/core/supervisor/ledger-state.d.ts +55 -0
  200. package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
  201. package/dist/core/supervisor/ledger-state.js +0 -0
  202. package/dist/core/supervisor/service.d.ts.map +1 -1
  203. package/dist/core/supervisor/service.js +120 -7
  204. package/dist/core/supervisor/storage.d.ts +3 -1
  205. package/dist/core/supervisor/storage.d.ts.map +1 -1
  206. package/dist/core/supervisor/storage.js +4 -0
  207. package/dist/core/workflow/admission.d.ts +3 -0
  208. package/dist/core/workflow/admission.d.ts.map +1 -1
  209. package/dist/core/workflow/admission.js +28 -16
  210. package/dist/core/workflow/engine.js +2 -2
  211. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  212. package/dist/core/workflow/live-session-v3.js +6 -1
  213. package/dist/core/workflow/proof.d.ts.map +1 -1
  214. package/dist/core/workflow/proof.js +4 -3
  215. package/dist/lib/browser/capture-fidelity.d.ts +7 -0
  216. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
  217. package/dist/lib/browser/capture-fidelity.js +17 -0
  218. package/dist/lib/browser/client.d.ts +9 -0
  219. package/dist/lib/browser/client.d.ts.map +1 -1
  220. package/dist/lib/browser/client.js +85 -37
  221. package/dist/lib/browser/index.d.ts +3 -1
  222. package/dist/lib/browser/index.d.ts.map +1 -1
  223. package/dist/lib/browser/index.js +2 -1
  224. package/dist/lib/browser/page-review-budget.d.ts +13 -0
  225. package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
  226. package/dist/lib/browser/page-review-budget.js +251 -0
  227. package/dist/lib/browser/page-review-contracts.d.ts +127 -0
  228. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
  229. package/dist/lib/browser/page-review-contracts.js +5 -0
  230. package/dist/lib/browser/page-review-evidence.d.ts +48 -0
  231. package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
  232. package/dist/lib/browser/page-review-evidence.js +340 -0
  233. package/dist/lib/browser/page-review-judge.d.ts +1 -1
  234. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  235. package/dist/lib/browser/page-review-judge.js +46 -38
  236. package/dist/lib/browser/page-review-pack.d.ts +44 -22
  237. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  238. package/dist/lib/browser/page-review-pack.js +116 -36
  239. package/dist/lib/browser/page-review-source.d.ts +12 -0
  240. package/dist/lib/browser/page-review-source.d.ts.map +1 -0
  241. package/dist/lib/browser/page-review-source.js +22 -0
  242. package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
  243. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  244. package/dist/lib/browser/qa-run-contracts.js +48 -14
  245. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  246. package/dist/lib/browser/qa-run.js +215 -68
  247. package/dist/lib/browser/qa-snapshot.d.ts +6 -0
  248. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
  249. package/dist/lib/browser/qa-snapshot.js +259 -48
  250. package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
  251. package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
  252. package/dist/lib/browser/review-capture-transaction.js +79 -0
  253. package/dist/lib/browser/standalone-html.d.ts.map +1 -1
  254. package/dist/lib/browser/standalone-html.js +4 -2
  255. package/dist/lib/browser/thumbnail-association.d.ts +34 -0
  256. package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
  257. package/dist/lib/browser/thumbnail-association.js +158 -0
  258. package/dist/lib/browser/tiling.d.ts +30 -0
  259. package/dist/lib/browser/tiling.d.ts.map +1 -1
  260. package/dist/lib/browser/tiling.js +38 -0
  261. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  262. package/dist/lib/docs-metadata-managed.js +4 -1
  263. package/dist/lib/identities/assume.d.ts.map +1 -1
  264. package/dist/lib/identities/assume.js +7 -1
  265. package/dist/lib/instructions/templates.d.ts.map +1 -1
  266. package/dist/lib/instructions/templates.js +1 -0
  267. package/dist/lib/local-file-url.d.ts +12 -0
  268. package/dist/lib/local-file-url.d.ts.map +1 -1
  269. package/dist/lib/local-file-url.js +16 -2
  270. package/dist/lib/tunnel/state.d.ts +19 -4
  271. package/dist/lib/tunnel/state.d.ts.map +1 -1
  272. package/dist/lib/tunnel/state.js +53 -13
  273. package/package.json +7 -4
  274. package/schemas/config.schema.json +69 -2
  275. package/src/commands/agents.ts +42 -9
  276. package/src/commands/artifacts.ts +130 -6
  277. package/src/commands/backup.ts +287 -34
  278. package/src/commands/browse.ts +564 -267
  279. package/src/commands/diagnostics.ts +6 -3
  280. package/src/commands/ledger-v3.ts +20 -3
  281. package/src/commands/qa-run.ts +37 -3
  282. package/src/commands/qa-status.ts +11 -2
  283. package/src/commands/resources.ts +23 -3
  284. package/src/commands/review-pack.ts +165 -62
  285. package/src/commands/tunnel.ts +2 -1
  286. package/src/core/agents/canonical-emit.ts +4 -2
  287. package/src/core/agents/cli.ts +14 -6
  288. package/src/core/agents/live-authority-v3.ts +7 -3
  289. package/src/core/agents/live-lifecycle-v3.ts +2 -2
  290. package/src/core/agents/render/prompt-context.ts +68 -14
  291. package/src/core/agents/session-finalizer-v3.ts +2 -2
  292. package/src/core/agents/state/live-coordination-view.ts +4 -4
  293. package/src/core/agents/state/names.ts +92 -7
  294. package/src/core/agents/state/stale-sweep.ts +22 -8
  295. package/src/core/agents/turn-elapsed.ts +135 -0
  296. package/src/core/artifacts/activity.ts +71 -0
  297. package/src/core/artifacts/delivery-card.ts +375 -0
  298. package/src/core/artifacts/index.ts +385 -91
  299. package/src/core/artifacts/mutation-lock.ts +106 -0
  300. package/src/core/backup/host-snapshot.ts +76 -0
  301. package/src/core/config.ts +66 -13
  302. package/src/core/diagnostics/advice.ts +148 -120
  303. package/src/core/diagnostics/bundle.ts +138 -52
  304. package/src/core/diagnostics/comparison.ts +6 -2
  305. package/src/core/diagnostics/contract.ts +39 -42
  306. package/src/core/diagnostics/index.ts +2 -0
  307. package/src/core/diagnostics/pressure-contract.ts +294 -0
  308. package/src/core/diagnostics/pressure.ts +1361 -0
  309. package/src/core/diagnostics/replay.ts +116 -11
  310. package/src/core/events/v3/adapter-id.ts +12 -0
  311. package/src/core/events/v3/archive-retention.ts +3 -6
  312. package/src/core/events/v3/bootstrap.ts +163 -16
  313. package/src/core/events/v3/capabilities.ts +75 -25
  314. package/src/core/events/v3/capability-drift.ts +2 -2
  315. package/src/core/events/v3/control-witness.ts +172 -41
  316. package/src/core/events/v3/control.ts +34 -2
  317. package/src/core/events/v3/index.ts +3 -0
  318. package/src/core/events/v3/live-feed.ts +23 -5
  319. package/src/core/events/v3/live-routing.ts +14 -2
  320. package/src/core/events/v3/producers/command-recorder.ts +4 -4
  321. package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
  322. package/src/core/events/v3/producers/hook-base.ts +21 -16
  323. package/src/core/events/v3/producers/hook.ts +21 -4
  324. package/src/core/events/v3/producers/intake.ts +6 -6
  325. package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
  326. package/src/core/events/v3/producers/recorder.ts +50 -21
  327. package/src/core/events/v3/reader.ts +14 -13
  328. package/src/core/events/v3/runtime-build.ts +3 -2
  329. package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
  330. package/src/core/events/v3/turn-telemetry.ts +4 -4
  331. package/src/core/hooks/adapter/output.ts +50 -36
  332. package/src/core/hooks/backup-schedule.ts +147 -0
  333. package/src/core/hooks/cli.ts +65 -14
  334. package/src/core/hooks/codex-wsl-bridge.ts +21 -5
  335. package/src/core/hooks/index.ts +1 -0
  336. package/src/core/hooks/prompt-context/contract.ts +177 -0
  337. package/src/core/hooks/prompt-context/index.ts +22 -0
  338. package/src/core/hooks/prompt-context/runner.ts +303 -0
  339. package/src/core/qa-artifacts.ts +29 -3
  340. package/src/core/resources/contract.ts +76 -3
  341. package/src/core/resources/index.ts +1 -0
  342. package/src/core/resources/native.ts +481 -0
  343. package/src/core/resources/sampler.ts +46 -1
  344. package/src/core/resources/status.ts +717 -0
  345. package/src/core/resources/system.ts +530 -0
  346. package/src/core/semantic/service.ts +37 -3
  347. package/src/core/storage/builtins.ts +41 -0
  348. package/src/core/supervisor/contract.ts +45 -3
  349. package/src/core/supervisor/findings.ts +159 -0
  350. package/src/core/supervisor/history.ts +15 -0
  351. package/src/core/supervisor/ledger-state.ts +0 -0
  352. package/src/core/supervisor/service.ts +133 -5
  353. package/src/core/supervisor/storage.ts +7 -0
  354. package/src/core/workflow/admission.ts +33 -17
  355. package/src/core/workflow/engine.ts +2 -2
  356. package/src/core/workflow/live-session-v3.ts +6 -8
  357. package/src/core/workflow/proof.ts +4 -3
  358. package/src/lib/browser/capture-fidelity.ts +23 -0
  359. package/src/lib/browser/client.ts +96 -44
  360. package/src/lib/browser/index.ts +15 -1
  361. package/src/lib/browser/page-review-budget.ts +309 -0
  362. package/src/lib/browser/page-review-contracts.ts +129 -0
  363. package/src/lib/browser/page-review-evidence.ts +465 -0
  364. package/src/lib/browser/page-review-judge.ts +56 -40
  365. package/src/lib/browser/page-review-pack.ts +181 -43
  366. package/src/lib/browser/page-review-source.ts +30 -0
  367. package/src/lib/browser/qa-run-contracts.ts +62 -18
  368. package/src/lib/browser/qa-run.ts +259 -71
  369. package/src/lib/browser/qa-snapshot.ts +289 -48
  370. package/src/lib/browser/review-capture-transaction.ts +106 -0
  371. package/src/lib/browser/standalone-html.ts +4 -2
  372. package/src/lib/browser/thumbnail-association.ts +190 -0
  373. package/src/lib/browser/tiling.ts +63 -0
  374. package/src/lib/docs-metadata-managed.ts +4 -1
  375. package/src/lib/identities/assume.ts +12 -4
  376. package/src/lib/instructions/templates.ts +1 -0
  377. package/src/lib/local-file-url.ts +17 -2
  378. package/src/lib/tunnel/state.ts +70 -13
@@ -8,6 +8,7 @@ import { resolveBinName } from "../core/config.ts";
8
8
  import {
9
9
  CAPTURE_FIDELITY_MISMATCH_THRESHOLD,
10
10
  compareBand,
11
+ cropNativePng,
11
12
  decideFidelity,
12
13
  type FidelityProbe,
13
14
  pngDimensions,
@@ -43,8 +44,19 @@ import {
43
44
  writeNetscapeCookieFile,
44
45
  wslHeadedLaunchArgs,
45
46
  } from "../lib/browser/index.ts";
47
+ import {
48
+ allocateTileBudget,
49
+ validatePageReviewAllocation,
50
+ } from "../lib/browser/page-review-budget.ts";
51
+ import {
52
+ PAGE_REVIEW_CAPTURE_PLAN_SCHEMA,
53
+ type PageReviewCapturePlan,
54
+ type PageReviewContextAllocation,
55
+ type PageReviewSourceEvidence,
56
+ } from "../lib/browser/page-review-contracts.ts";
46
57
  import {
47
58
  findPackTile,
59
+ gateHitsFromEnvelope,
48
60
  type PageReviewCaptureFidelity,
49
61
  type PageReviewContextRecord,
50
62
  type PageReviewExpandedTileRecord,
@@ -53,6 +65,7 @@ import {
53
65
  writePackContext,
54
66
  writePackExpandedTile,
55
67
  } from "../lib/browser/page-review-pack.ts";
68
+ import { retainPageReviewSourceIdentity } from "../lib/browser/page-review-source.ts";
56
69
  import {
57
70
  buildQaManifest,
58
71
  classifySignatures,
@@ -71,10 +84,23 @@ import {
71
84
  rubricDigest,
72
85
  } from "../lib/browser/qa-reuse.ts";
73
86
  import { loadQaSnapshot, resolveQaBaseline, saveQaSnapshot } from "../lib/browser/qa-snapshot.ts";
87
+ import {
88
+ allocateCaptureReservation,
89
+ assertGatePlanStable,
90
+ CaptureSourceChanged,
91
+ runCaptureTransaction,
92
+ writeCaptureTransaction,
93
+ } from "../lib/browser/review-capture-transaction.ts";
74
94
  import {
75
95
  type BrowserSessionServer,
76
96
  startBrowserSessionServer,
77
97
  } from "../lib/browser/session-control.ts";
98
+ import {
99
+ type CaptureFingerprint,
100
+ captureThumbnailFingerprint,
101
+ registerCaptureThumbnail,
102
+ } from "../lib/browser/thumbnail-association.ts";
103
+ import { reviewCandidateRects } from "../lib/browser/tiling.ts";
78
104
  import {
79
105
  type DiffResult,
80
106
  diffAgainstBaseline,
@@ -108,6 +134,16 @@ const DEFAULT_STORE = resolve(homedir(), ".cache", "harnery", "cookies.json");
108
134
  const FALLBACK_OUT_PREFIX = resolve(homedir(), ".cache", "harnery", "browse", "last");
109
135
 
110
136
  interface BrowseOpts {
137
+ reviewPackPlan?: boolean;
138
+ reviewPackAllocation?: string;
139
+ reviewPackReservation?: string;
140
+ captureTransactionAllocation?: PageReviewContextAllocation;
141
+ captureTransaction?: {
142
+ attempts: Array<{ attempt: number; status: string; source_digest?: string; error?: string }>;
143
+ source_digest?: string;
144
+ };
145
+ capturePlanResult?: PageReviewCapturePlan;
146
+ captureSourceEvidence?: PageReviewSourceEvidence;
111
147
  out?: string;
112
148
  // Commander expands `--no-X` into `opts.x = false` (default true), not
113
149
  // `opts.noX`. So `--no-screenshot` toggles `screenshot`, `--no-full-page`
@@ -236,7 +272,7 @@ interface BrowseOpts {
236
272
  reviewPack?: string;
237
273
  reviewPackContext?: string;
238
274
  reviewPackScope?: string[];
239
- reviewPackBands?: boolean;
275
+
240
276
  /** Re-capture ONE existing tile of --review-pack-context at --device-scale-factor. */
241
277
  reviewPackExpand?: string;
242
278
  /** Gate-hit rectangles (`x,y,w,h` strings) whose bands are captured past the tile cap. */
@@ -725,6 +761,19 @@ export function registerBrowseCommand(
725
761
  "vision call. A judge stage (`review-pack judge`, or qa-run) reviews the tiles from disk " +
726
762
  "after every browser has closed. Tiling knobs are the --check-critique-* flags.",
727
763
  )
764
+ .option(
765
+ "--review-pack-plan",
766
+ "Emit all native capture candidates and gate associations without capturing tiles. " +
767
+ "With --out, retain private source identity inputs beside the output for diagnostics.",
768
+ )
769
+ .option(
770
+ "--review-pack-allocation <json-file>",
771
+ "Capture exactly the selected IDs in a verified context allocation.",
772
+ )
773
+ .option(
774
+ "--review-pack-reservation <json-file>",
775
+ "Reserve the preliminary allocation's tile count, then rerun supplied gates and allocate against the same page before capture. Full source checks and native coverage remain required.",
776
+ )
728
777
  .option(
729
778
  "--review-pack-context <id>",
730
779
  "Context id inside the pack (default <viewport>-<theme>-<state> from --viewport, " +
@@ -736,11 +785,6 @@ export function registerBrowseCommand(
736
785
  (value: string, previous: string[] = []) => [...previous, value],
737
786
  [] as string[],
738
787
  )
739
- .option(
740
- "--no-review-pack-bands",
741
- "Skip full-page bands in the pack and keep only --review-pack-scope tiles (a scoped " +
742
- "visual pass).",
743
- )
744
788
  .option(
745
789
  "--review-pack-expand <tile-id>",
746
790
  "Instead of capturing a context, re-render ONE existing tile of --review-pack-context " +
@@ -749,9 +793,9 @@ export function registerBrowseCommand(
749
793
  )
750
794
  .option(
751
795
  "--review-pack-hit-rect <x,y,w,h>",
752
- "Document-space rectangle (integer px) a deterministic gate already flagged. Every " +
753
- "full-page band that contains it is captured even past --check-critique-max-tiles, " +
754
- `labelled \`hit band N\` (repeatable, at most ${REVIEW_PACK_HIT_RECT_MAX}).`,
796
+ "Document-space rectangle (integer px) a deterministic gate already flagged. " +
797
+ "Intersecting bands are prioritized within the hard tile cap " +
798
+ `(repeatable, at most ${REVIEW_PACK_HIT_RECT_MAX}).`,
755
799
  (value: string, previous: string[] = []) => [...previous, value],
756
800
  [] as string[],
757
801
  )
@@ -815,6 +859,17 @@ async function runBrowse(
815
859
  const headed = opts.login || opts.headed;
816
860
  const viewport = parseViewport(opts.viewport ?? "desktop");
817
861
  const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
862
+ if (opts.reviewPackAllocation && !opts.reviewPack) {
863
+ throw new Error("--review-pack-allocation requires --review-pack <dir>.");
864
+ }
865
+ if (
866
+ opts.reviewPackReservation &&
867
+ (!opts.reviewPack || opts.reviewPackAllocation || opts.reviewPackExpand || opts.checkCritique)
868
+ ) {
869
+ throw new Error(
870
+ "--review-pack-reservation requires an ordinary --review-pack capture without allocation, expansion or inline critique.",
871
+ );
872
+ }
818
873
  if (opts.reviewPackExpand !== undefined) {
819
874
  if (opts.reviewPack === undefined || opts.reviewPackContext === undefined) {
820
875
  throw new Error(
@@ -905,126 +960,216 @@ async function runBrowse(
905
960
  evalResult = await browser.evaluate<unknown>(opts.evaluate);
906
961
  }
907
962
 
963
+ if (opts.reviewPackPlan || opts.reviewPack) await browser.waitForReviewReady();
964
+
908
965
  // Run visibility checks AFTER any --batch interactions but BEFORE the
909
966
  // screenshot. Annotation injection happens between sampling and capture
910
967
  // so the boxes show on the saved PNG; they're cleared post-screenshot
911
968
  // so the live profile state isn't polluted.
912
- let visibility: VisibilityResult[] | undefined;
913
- if (opts.checkVisible && opts.checkVisible.length > 0) {
914
- visibility = await browser.checkVisibility(opts.checkVisible, {
915
- sampleGrid: Number.parseInt(opts.checkVisibleSampleGrid ?? "3", 10),
916
- });
917
- if (opts.checkVisibleAnnotate !== false) {
918
- await browser.annotateVisibility(visibility);
969
+ const collectGates = async (attempt = 0) => {
970
+ const before = opts.reviewPackReservation
971
+ ? await buildReviewCapturePlan(browser, opts)
972
+ : undefined;
973
+ let visibility: VisibilityResult[] | undefined;
974
+ if (opts.checkVisible && opts.checkVisible.length > 0) {
975
+ visibility = await browser.checkVisibility(opts.checkVisible, {
976
+ sampleGrid: Number.parseInt(opts.checkVisibleSampleGrid ?? "3", 10),
977
+ });
978
+ if (opts.checkVisibleAnnotate !== false && !opts.reviewPackPlan && !opts.reviewPack) {
979
+ await browser.annotateVisibility(visibility);
980
+ }
919
981
  }
920
- }
921
982
 
922
- let widths: WidthResult[] | undefined;
923
- if (opts.checkWidth && opts.checkWidth.length > 0) {
924
- widths = await browser.checkWidth(opts.checkWidth);
925
- }
926
- let overflow: OverflowResult | undefined;
927
- if (opts.checkOverflow) {
928
- overflow = await browser.checkOverflow();
929
- }
930
- let runts: RuntsResult | undefined;
931
- if (opts.checkRunts) {
932
- runts = await browser.checkRunts({
933
- scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
934
- minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
935
- });
936
- }
937
- const hasLayoutLint =
938
- (opts.checkAlign?.length ?? 0) > 0 ||
939
- (opts.checkGap?.length ?? 0) > 0 ||
940
- (opts.checkClip?.length ?? 0) > 0 ||
941
- (opts.checkOverlap?.length ?? 0) > 0 ||
942
- (opts.checkCrowd?.length ?? 0) > 0;
943
- let layoutLint: LayoutLintResult | undefined;
944
- if (hasLayoutLint) {
945
- layoutLint = await browser.checkLayoutLint({
946
- align: (opts.checkAlign ?? []).map((selector) => ({
947
- selector,
948
- axis: alignAxis,
949
- tolerancePx: alignThreshold,
950
- })),
951
- gap: (opts.checkGap ?? []).map((selector) => ({
952
- selector,
953
- axis: gapAxis,
954
- tolerancePx: gapThreshold,
955
- expectedGapPx: gapExpected,
956
- })),
957
- clip: (opts.checkClip ?? []).map((selector) => ({
958
- selector,
959
- tolerancePx: clipThreshold,
960
- })),
961
- overlap: (opts.checkOverlap ?? []).map((selector) => ({
962
- selector,
963
- tolerancePx: overlapThreshold,
964
- })),
965
- crowd: (opts.checkCrowd ?? []).map((selector) => ({
966
- selector,
967
- minGapPx: crowdMin,
968
- })),
969
- });
970
- }
971
- let hit: TargetSizeResult[] | undefined;
972
- if (opts.checkHit && opts.checkHit.length > 0) {
973
- hit = await browser.checkTargetSize(opts.checkHit, hitProfile);
974
- }
975
- const hasContentChecks =
976
- opts.checkPlaceholder !== undefined ||
977
- opts.checkImages !== undefined ||
978
- opts.checkTruncation !== undefined ||
979
- opts.checkContrast !== undefined;
980
- let content: ContentChecksResult | undefined;
981
- if (hasContentChecks) {
982
- content = await browser.checkContent({
983
- placeholder:
984
- opts.checkPlaceholder !== undefined
985
- ? { scope: contentScope(opts.checkPlaceholder) }
986
- : null,
987
- image:
988
- opts.checkImages !== undefined
989
- ? {
990
- scope: contentScope(opts.checkImages),
991
- tolerance: parseNonNegativeNumber(
992
- opts.checkImagesTolerance ?? "0.1",
993
- "--check-images-tolerance",
994
- ),
995
- }
996
- : null,
997
- truncation:
998
- opts.checkTruncation !== undefined
999
- ? {
1000
- scope: contentScope(opts.checkTruncation),
1001
- tolerance: parseNonNegativeNumber(
1002
- opts.checkTruncationTolerance ?? "2",
1003
- "--check-truncation-tolerance",
1004
- ),
1005
- }
1006
- : null,
1007
- contrast:
1008
- opts.checkContrast !== undefined ? { scope: contentScope(opts.checkContrast) } : null,
1009
- });
1010
- }
1011
- // Diff-aware QA planning. Signature capture must also happen BEFORE any
1012
- // annotation overlays are injected — an annotation box is a DOM change.
1013
- let qaPlan: QaPlanReport | undefined;
1014
- let qaCapture: QaCaptureState | undefined;
1015
- if (opts.qaPlan || opts.qaSnapshot) {
1016
- const qa = await runQaPlanning(browser, url, navResult.url, opts);
1017
- qaPlan = qa.report;
1018
- qaCapture = qa.capture;
1019
- if (qaPlan?.manifest) {
1020
- const m = qaPlan.manifest;
1021
- emit.log(
1022
- `qa-plan: class=${m.change_class} scopes=${m.scopes.map((s) => s.selector).join(",") || "-"} ` +
1023
- `contexts=${m.contexts.length} model-calls<=${m.predicted.model_calls_ceiling} baseline=${m.baseline_source}`,
1024
- "info",
983
+ let widths: WidthResult[] | undefined;
984
+ if (opts.checkWidth && opts.checkWidth.length > 0) {
985
+ widths = await browser.checkWidth(opts.checkWidth);
986
+ }
987
+ let overflow: OverflowResult | undefined;
988
+ if (opts.checkOverflow) {
989
+ overflow = await browser.checkOverflow();
990
+ }
991
+ let runts: RuntsResult | undefined;
992
+ if (opts.checkRunts) {
993
+ runts = await browser.checkRunts({
994
+ scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
995
+ minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
996
+ });
997
+ }
998
+ const hasLayoutLint =
999
+ (opts.checkAlign?.length ?? 0) > 0 ||
1000
+ (opts.checkGap?.length ?? 0) > 0 ||
1001
+ (opts.checkClip?.length ?? 0) > 0 ||
1002
+ (opts.checkOverlap?.length ?? 0) > 0 ||
1003
+ (opts.checkCrowd?.length ?? 0) > 0;
1004
+ let layoutLint: LayoutLintResult | undefined;
1005
+ if (hasLayoutLint) {
1006
+ layoutLint = await browser.checkLayoutLint({
1007
+ align: (opts.checkAlign ?? []).map((selector) => ({
1008
+ selector,
1009
+ axis: alignAxis,
1010
+ tolerancePx: alignThreshold,
1011
+ })),
1012
+ gap: (opts.checkGap ?? []).map((selector) => ({
1013
+ selector,
1014
+ axis: gapAxis,
1015
+ tolerancePx: gapThreshold,
1016
+ expectedGapPx: gapExpected,
1017
+ })),
1018
+ clip: (opts.checkClip ?? []).map((selector) => ({
1019
+ selector,
1020
+ tolerancePx: clipThreshold,
1021
+ })),
1022
+ overlap: (opts.checkOverlap ?? []).map((selector) => ({
1023
+ selector,
1024
+ tolerancePx: overlapThreshold,
1025
+ })),
1026
+ crowd: (opts.checkCrowd ?? []).map((selector) => ({
1027
+ selector,
1028
+ minGapPx: crowdMin,
1029
+ })),
1030
+ });
1031
+ }
1032
+ let hit: TargetSizeResult[] | undefined;
1033
+ if (opts.checkHit && opts.checkHit.length > 0) {
1034
+ hit = await browser.checkTargetSize(opts.checkHit, hitProfile);
1035
+ }
1036
+ const hasContentChecks =
1037
+ opts.checkPlaceholder !== undefined ||
1038
+ opts.checkImages !== undefined ||
1039
+ opts.checkTruncation !== undefined ||
1040
+ opts.checkContrast !== undefined;
1041
+ let content: ContentChecksResult | undefined;
1042
+ if (hasContentChecks) {
1043
+ content = await browser.checkContent({
1044
+ placeholder:
1045
+ opts.checkPlaceholder !== undefined
1046
+ ? { scope: contentScope(opts.checkPlaceholder) }
1047
+ : null,
1048
+ image:
1049
+ opts.checkImages !== undefined
1050
+ ? {
1051
+ scope: contentScope(opts.checkImages),
1052
+ tolerance: parseNonNegativeNumber(
1053
+ opts.checkImagesTolerance ?? "0.1",
1054
+ "--check-images-tolerance",
1055
+ ),
1056
+ }
1057
+ : null,
1058
+ truncation:
1059
+ opts.checkTruncation !== undefined
1060
+ ? {
1061
+ scope: contentScope(opts.checkTruncation),
1062
+ tolerance: parseNonNegativeNumber(
1063
+ opts.checkTruncationTolerance ?? "2",
1064
+ "--check-truncation-tolerance",
1065
+ ),
1066
+ }
1067
+ : null,
1068
+ contrast:
1069
+ opts.checkContrast !== undefined ? { scope: contentScope(opts.checkContrast) } : null,
1070
+ });
1071
+ }
1072
+ // Diff-aware QA planning. Signature capture must also happen BEFORE any
1073
+ // annotation overlays are injected — an annotation box is a DOM change.
1074
+ let qaPlan: QaPlanReport | undefined;
1075
+ let qaCapture: QaCaptureState | undefined;
1076
+ if (opts.qaPlan || opts.qaSnapshot) {
1077
+ const qa = await runQaPlanning(browser, url, navResult.url, opts);
1078
+ qaPlan = qa.report;
1079
+ qaCapture = qa.capture;
1080
+ if (qaPlan?.manifest) {
1081
+ const m = qaPlan.manifest;
1082
+ emit.log(
1083
+ `qa-plan: class=${m.change_class} scopes=${m.scopes.map((s) => s.selector).join(",") || "-"} ` +
1084
+ `contexts=${m.contexts.length} model-calls<=${m.predicted.model_calls_ceiling} baseline=${m.baseline_source}`,
1085
+ "info",
1086
+ );
1087
+ }
1088
+ }
1089
+ let asserts: AssertResult[] | undefined;
1090
+ if (opts.assert && opts.assert.length > 0) {
1091
+ const specs = opts.assert.map(parseAssertSpec);
1092
+ asserts = await browser.checkAsserts(specs);
1093
+ }
1094
+ if (opts.reviewPackPlan || opts.reviewPackReservation) {
1095
+ const gateEnvelope: Record<string, unknown> = { runts, overflow, hit, ...layoutLint };
1096
+ if (content) assignContent(gateEnvelope, content);
1097
+ opts.capturePlanResult = await buildReviewCapturePlan(browser, opts);
1098
+ const hits = gateHitsFromEnvelope(gateEnvelope, Number.POSITIVE_INFINITY);
1099
+ for (const candidate of opts.capturePlanResult.candidates) {
1100
+ const r = candidate.rect;
1101
+ candidate.gate_hits = hits.flatMap((hit, index) => {
1102
+ const dpr = opts.capturePlanResult!.dpr;
1103
+ const h = {
1104
+ x: hit.rect.x * dpr,
1105
+ y: hit.rect.y * dpr,
1106
+ width: Math.max(1, hit.rect.width * dpr),
1107
+ height: Math.max(1, hit.rect.height * dpr),
1108
+ };
1109
+ return r.x < h.x + h.width &&
1110
+ r.x + r.width > h.x &&
1111
+ r.y < h.y + h.height &&
1112
+ r.y + r.height > h.y
1113
+ ? [{ check_id: `${hit.rule}:${index + 1}`, severity: "high" as const }]
1114
+ : [];
1115
+ });
1116
+ }
1117
+ }
1118
+
1119
+ if (opts.reviewPackReservation) {
1120
+ const prefix = opts.out ?? resolve(opts.reviewPack!, opts.reviewPackContext ?? "capture");
1121
+ mkdirSync(dirname(prefix), { recursive: true, mode: 0o700 });
1122
+ writeFileSync(
1123
+ `${prefix}.attempt-${attempt}.gates.json`,
1124
+ JSON.stringify(
1125
+ {
1126
+ attempt,
1127
+ source_before: before?.source_digest,
1128
+ source_after: opts.capturePlanResult?.source_digest,
1129
+ source_evidence: opts.captureSourceEvidence,
1130
+ visibility,
1131
+ widths,
1132
+ overflow,
1133
+ runts,
1134
+ layoutLint,
1135
+ hit,
1136
+ content,
1137
+ asserts,
1138
+ },
1139
+ null,
1140
+ 2,
1141
+ ),
1142
+ { mode: 0o600 },
1025
1143
  );
1026
1144
  }
1027
- }
1145
+ if (before) assertGatePlanStable(before, opts.capturePlanResult!);
1146
+ return {
1147
+ visibility,
1148
+ widths,
1149
+ overflow,
1150
+ runts,
1151
+ layoutLint,
1152
+ hit,
1153
+ content,
1154
+ qaPlan,
1155
+ qaCapture,
1156
+ asserts,
1157
+ };
1158
+ };
1159
+ let {
1160
+ visibility,
1161
+ widths,
1162
+ overflow,
1163
+ runts,
1164
+ layoutLint,
1165
+ hit,
1166
+ content,
1167
+ qaPlan,
1168
+ qaCapture,
1169
+ asserts,
1170
+ }: Partial<Awaited<ReturnType<typeof collectGates>>> = opts.reviewPackReservation
1171
+ ? {}
1172
+ : await collectGates();
1028
1173
  // Vision critique. Capture tiles BEFORE any annotation overlays are injected
1029
1174
  // so the model sees the real page, not our boxes.
1030
1175
  let critique: CritiqueResult | undefined;
@@ -1095,6 +1240,62 @@ async function runBrowse(
1095
1240
  let reviewPack: ReviewPackReport | undefined;
1096
1241
  if (opts.reviewPack !== undefined && opts.reviewPackExpand !== undefined) {
1097
1242
  reviewPack = await expandReviewPackTile(browser, opts, deviceScaleFactor ?? 1);
1243
+ } else if (opts.reviewPackReservation) {
1244
+ const reservation = JSON.parse(readFileSync(resolve(opts.reviewPackReservation), "utf8"));
1245
+ opts.captureTransaction = { attempts: [] };
1246
+ const persistTransaction = () => {
1247
+ const prefix = opts.out ?? resolve(opts.reviewPack!, opts.reviewPackContext ?? "capture");
1248
+ mkdirSync(dirname(prefix), { recursive: true, mode: 0o700 });
1249
+ writeFileSync(
1250
+ `${prefix}.capture-transaction.json`,
1251
+ JSON.stringify(opts.captureTransaction, null, 2),
1252
+ { mode: 0o600 },
1253
+ );
1254
+ };
1255
+ reviewPack = await runCaptureTransaction(
1256
+ async (attempt) => {
1257
+ await browser.waitForReviewReady();
1258
+ ({
1259
+ visibility,
1260
+ widths,
1261
+ overflow,
1262
+ runts,
1263
+ layoutLint,
1264
+ hit,
1265
+ content,
1266
+ qaPlan,
1267
+ qaCapture,
1268
+ asserts,
1269
+ } = await collectGates(attempt));
1270
+ opts.captureTransactionAllocation = allocateCaptureReservation(
1271
+ reservation,
1272
+ opts.capturePlanResult!,
1273
+ );
1274
+ opts.captureTransaction!.attempts.push({
1275
+ attempt,
1276
+ status: "gated",
1277
+ source_digest: opts.capturePlanResult!.source_digest,
1278
+ });
1279
+ persistTransaction();
1280
+ },
1281
+ async () => {
1282
+ const captured = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
1283
+ opts.captureTransaction!.source_digest =
1284
+ opts.captureTransactionAllocation!.plan.source_digest;
1285
+ opts.captureTransaction!.attempts.at(-1)!.status = "captured";
1286
+ persistTransaction();
1287
+ return captured;
1288
+ },
1289
+ (attempt, error) => {
1290
+ opts.captureTransaction!.attempts.push({
1291
+ attempt,
1292
+ status: "failed",
1293
+ error: error instanceof Error ? error.message : String(error),
1294
+ source_digest: opts.capturePlanResult?.source_digest,
1295
+ });
1296
+ persistTransaction();
1297
+ },
1298
+ );
1098
1299
  } else if (opts.reviewPack !== undefined) {
1099
1300
  reviewPack = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
1100
1301
  }
@@ -1153,11 +1354,7 @@ async function runBrowse(
1153
1354
  "info",
1154
1355
  );
1155
1356
  }
1156
- let asserts: AssertResult[] | undefined;
1157
- if (opts.assert && opts.assert.length > 0) {
1158
- const specs = opts.assert.map(parseAssertSpec);
1159
- asserts = await browser.checkAsserts(specs);
1160
- }
1357
+
1161
1358
  const widthThreshold = Number.parseFloat(opts.checkWidthThreshold ?? "0.9");
1162
1359
  const annotateWidth = widths && opts.checkWidthAnnotate !== false;
1163
1360
  const annotateOverflow = overflow && opts.checkOverflowAnnotate !== false;
@@ -1578,6 +1775,9 @@ async function runPrintMode(
1578
1775
  if (qaPlan) result.qaPlan = qaPlan;
1579
1776
  if (qaReuse) result.qaReuse = qaReuse;
1580
1777
  if (reviewPack) result.reviewPack = reviewPack;
1778
+ if (opts.capturePlanResult) result.review_pack_capture_plan = opts.capturePlanResult;
1779
+ if (opts.captureSourceEvidence) result.review_pack_source_evidence = opts.captureSourceEvidence;
1780
+ if (opts.captureTransaction) result.review_pack_transaction = opts.captureTransaction;
1581
1781
  if (batchResult && batchResult.clipboardReads.length > 0) {
1582
1782
  result.batchClipboardReads = batchResult.clipboardReads;
1583
1783
  }
@@ -1626,6 +1826,7 @@ async function runTrioMode(
1626
1826
  const skipScreenshot = opts.screenshot === false;
1627
1827
  let pngPath: string | undefined;
1628
1828
  let pngBytes: number | undefined;
1829
+ let pngFingerprint: CaptureFingerprint | null = null;
1629
1830
  let captureEvalResult: unknown;
1630
1831
  let captureViewport: { width: number; height: number } | undefined;
1631
1832
  let captureEvidence: unknown;
@@ -1642,6 +1843,8 @@ async function runTrioMode(
1642
1843
  } else {
1643
1844
  pngBytes = await browser.screenshot(pngPath, { fullPage: opts.fullPage !== false });
1644
1845
  }
1846
+ if (!opts.selector && opts.fullPage !== false)
1847
+ pngFingerprint = captureThumbnailFingerprint(pngPath);
1645
1848
  written.push(pngPath);
1646
1849
  } else if (opts.captureEvaluate) {
1647
1850
  throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot.");
@@ -1665,6 +1868,19 @@ async function runTrioMode(
1665
1868
  }
1666
1869
  written.push(htmlPath);
1667
1870
 
1871
+ const thumbnailAssociation =
1872
+ snapshot && pngPath && pngFingerprint
1873
+ ? registerCaptureThumbnail({
1874
+ htmlPath,
1875
+ pngPath,
1876
+ html: snapshot.html,
1877
+ preview: pngFingerprint,
1878
+ fullDocument: !opts.selector && opts.fullPage !== false,
1879
+ stylesheetsLinked: snapshot.stylesheetsLinked,
1880
+ resourcesLinked: snapshot.resourcesLinked,
1881
+ })
1882
+ : null;
1883
+
1668
1884
  const diag = browser.diagnostics();
1669
1885
  const jsonPath = `${prefix}.json`;
1670
1886
  const envelope: Record<string, unknown> = {
@@ -1679,6 +1895,7 @@ async function runTrioMode(
1679
1895
  ...summarizeDiagnostics(diag),
1680
1896
  };
1681
1897
  if (pngBytes !== undefined) envelope.screenshotBytes = pngBytes;
1898
+ if (thumbnailAssociation) envelope.thumbnailAssociation = thumbnailAssociation;
1682
1899
  if (snapshot) {
1683
1900
  envelope.htmlSnapshot = {
1684
1901
  stylesheetsInlined: snapshot.stylesheetsInlined,
@@ -1714,6 +1931,9 @@ async function runTrioMode(
1714
1931
  if (qaPlan) envelope.qaPlan = qaPlan;
1715
1932
  if (qaReuse) envelope.qaReuse = qaReuse;
1716
1933
  if (reviewPack) envelope.reviewPack = reviewPack;
1934
+ if (opts.capturePlanResult) envelope.review_pack_capture_plan = opts.capturePlanResult;
1935
+ if (opts.captureSourceEvidence) envelope.review_pack_source_evidence = opts.captureSourceEvidence;
1936
+ if (opts.captureTransaction) envelope.review_pack_transaction = opts.captureTransaction;
1717
1937
  if (batchResult && batchResult.clipboardReads.length > 0) {
1718
1938
  envelope.batchClipboardReads = batchResult.clipboardReads;
1719
1939
  }
@@ -2334,12 +2554,14 @@ async function expandReviewPackTile(
2334
2554
  const packDir = opts.reviewPack as string;
2335
2555
  const contextId = opts.reviewPackContext as string;
2336
2556
  const tileId = opts.reviewPackExpand as string;
2337
- const { tile } = findPackTile([readPackContext(packDir, contextId)], tileId, contextId);
2557
+ const originalContext = readPackContext(packDir, contextId);
2558
+ const { tile } = findPackTile([originalContext], tileId, contextId);
2559
+ const originalDpr = originalContext.dpr;
2338
2560
  const region = await browser.captureRegionByScroll({
2339
- x: tile.x,
2340
- y: tile.scrollY,
2341
- width: tile.width,
2342
- height: tile.height,
2561
+ x: tile.x / originalDpr,
2562
+ y: tile.scrollY / originalDpr,
2563
+ width: tile.width / originalDpr,
2564
+ height: tile.height / originalDpr,
2343
2565
  });
2344
2566
  const { record, expanded } = writePackExpandedTile(packDir, contextId, {
2345
2567
  tileId,
@@ -2368,9 +2590,9 @@ async function expandReviewPackTile(
2368
2590
  * agree the tiles stand and the record says `full-page`; when any disagrees,
2369
2591
  * every band is re-cut from its scrolled capture (same rects, same ids, only
2370
2592
  * the pixels change) and the record says `scrolled-bands`. A probe that
2371
- * cannot run, or a re-capture that comes back the wrong size (a page wider
2372
- * than the viewport), keeps the full-page crop for that band and says so in a
2373
- * warning. Runs while the capture browser is open; the judge never opens one.
2593
+ * cannot run, or a re-capture with the wrong size, leaves a missing probe;
2594
+ * the caller refuses to save that context. Runs while the capture browser
2595
+ * is open; the judge never opens one.
2374
2596
  */
2375
2597
  async function reconcileCaptureFidelity(
2376
2598
  browser: Browser,
@@ -2388,31 +2610,23 @@ async function reconcileCaptureFidelity(
2388
2610
  height: tile.height / dpr,
2389
2611
  });
2390
2612
  const shots = new Map<number, Buffer | undefined>();
2391
- const shoot = async (position: number): Promise<Buffer | undefined> => {
2392
- if (shots.has(position)) return shots.get(position);
2393
- const tile = tiles[position] as CritiqueTile;
2394
- let shot: Buffer | undefined;
2395
- try {
2396
- const captured = await browser.captureRegionByScroll(cssRect(tile));
2397
- const size = pngDimensions(captured);
2613
+ try {
2614
+ const captured = await browser.captureRegionsByScroll(tiles.map(cssRect));
2615
+ for (const [position, shot] of captured.entries()) {
2616
+ const tile = tiles[position];
2617
+ const size = pngDimensions(shot);
2398
2618
  if (Math.abs(size.width - tile.width) > 1 || Math.abs(size.height - tile.height) > 1) {
2399
2619
  warnings.push(
2400
- `review-pack: scrolled capture of ${tileId(position)} came back ` +
2401
- `${size.width}×${size.height} px for a ${tile.width}×${tile.height} px band; ` +
2402
- "its full-page crop was kept",
2620
+ `Native scrolled capture of ${tileId(position)} has different dimensions; probe incomplete.`,
2403
2621
  );
2404
- } else {
2405
- shot = captured;
2406
- }
2407
- } catch (err: unknown) {
2408
- warnings.push(
2409
- `review-pack: scrolled capture of ${tileId(position)} failed ` +
2410
- `(${err instanceof Error ? err.message : String(err)}); its full-page crop was kept`,
2411
- );
2622
+ } else shots.set(position, shot);
2412
2623
  }
2413
- shots.set(position, shot);
2414
- return shot;
2415
- };
2624
+ } catch (error) {
2625
+ warnings.push(
2626
+ `Native scrolled capture failed: ${error instanceof Error ? error.message : String(error)}; probe incomplete.`,
2627
+ );
2628
+ }
2629
+ const shoot = async (position: number) => shots.get(position);
2416
2630
  // Every band is probed. A two-band sample (top and middle) missed a raster
2417
2631
  // artifact that sat elsewhere on the page, and once every band has been
2418
2632
  // shot by scrolling, the fallback below costs nothing extra: the same shots
@@ -2452,11 +2666,90 @@ async function reconcileCaptureFidelity(
2452
2666
 
2453
2667
  /**
2454
2668
  * Capture this render into a page review pack: one full-page screenshot,
2455
- * banded tiles (unless --no-review-pack-bands), one tile set per
2669
+ * budgeted native tiles, one candidate set per
2456
2670
  * --review-pack-scope selector, the serialized DOM, and the QA signature —
2457
2671
  * all written to disk, with no vision call. The judge stage reads them back
2458
2672
  * once the browser is gone.
2459
2673
  */
2674
+ export const REVIEW_CAPTURE_RECIPE = "native-full-page-scroll-probe/v2";
2675
+
2676
+ async function buildReviewCapturePlan(
2677
+ browser: Browser,
2678
+ opts: BrowseOpts,
2679
+ ): Promise<PageReviewCapturePlan> {
2680
+ await browser.currentPage.evaluate(() => document.fonts.ready);
2681
+ const geometry = await browser.currentPage.evaluate(() => ({
2682
+ dpr: window.devicePixelRatio || 1,
2683
+ width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
2684
+ height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
2685
+ viewportWidth: window.innerWidth,
2686
+ viewportHeight: window.innerHeight,
2687
+ }));
2688
+ const { dpr } = geometry;
2689
+ const signature = await browser.qaSignature();
2690
+ // Attribute insertion order is serialization noise, not a source change.
2691
+ // Parse an inert document so canonicalization cannot mutate the rendered page.
2692
+ const canonicalDom = await browser.currentPage.evaluate((html) => {
2693
+ const document = new DOMParser().parseFromString(html, "text/html");
2694
+ for (const el of Array.from(document.querySelectorAll("*"))) {
2695
+ const attrs = Array.from(el.attributes)
2696
+ .map((a) => ({ name: a.name, value: a.value, ns: a.namespaceURI }))
2697
+ .sort((a, b) => a.name.localeCompare(b.name));
2698
+ for (const a of Array.from(el.attributes)) el.removeAttributeNode(a);
2699
+ for (const a of attrs) el.setAttributeNS(a.ns, a.name, a.value);
2700
+ }
2701
+ return document.documentElement.outerHTML;
2702
+ }, signature.domHtml);
2703
+ const source = retainPageReviewSourceIdentity(
2704
+ { nodes: signature.nodes, stylesheets: signature.stylesheets, dom: canonicalDom },
2705
+ opts.out ?? (opts.reviewPack ? resolve(opts.reviewPack, "source") : undefined),
2706
+ );
2707
+ opts.captureSourceEvidence = source.evidence;
2708
+ const atoms = (await browser.visualAtoms()).map((a) => ({
2709
+ ...a,
2710
+ top: Math.round(a.top * dpr),
2711
+ bottom: Math.round(a.bottom * dpr),
2712
+ }));
2713
+ const { band, overlap } = critiqueTilingKnobs(opts);
2714
+ const scopes = [];
2715
+ for (const selector of opts.reviewPackScope ?? []) {
2716
+ const rects = (await browser.elementTiles(selector)).map((r) => ({
2717
+ ...r,
2718
+ x: Math.round(r.x * dpr),
2719
+ y: Math.round(r.y * dpr),
2720
+ width: Math.round(r.width * dpr),
2721
+ height: Math.round(r.height * dpr),
2722
+ }));
2723
+ scopes.push({ selector, rects });
2724
+ }
2725
+ const theme = opts.qaTheme === "dark" ? "dark" : "light";
2726
+ const viewport = opts.viewport ?? "desktop",
2727
+ state = opts.qaState ?? "default";
2728
+ return {
2729
+ schema: PAGE_REVIEW_CAPTURE_PLAN_SCHEMA,
2730
+ context_id: opts.reviewPackContext ?? `${viewport}--${theme}--${state}`,
2731
+ viewport,
2732
+ viewport_width: geometry.viewportWidth,
2733
+ viewport_height: geometry.viewportHeight,
2734
+ theme,
2735
+ state,
2736
+ dpr,
2737
+ page_width: Math.round(geometry.width * dpr),
2738
+ page_height: Math.round(geometry.height * dpr),
2739
+ source_digest: source.digest,
2740
+ recipe_version: REVIEW_CAPTURE_RECIPE,
2741
+ required_scopes: opts.reviewPackScope ?? [],
2742
+ candidates: reviewCandidateRects({
2743
+ width: Math.round(geometry.width * dpr),
2744
+ height: Math.round(geometry.height * dpr),
2745
+ bandHeight: band,
2746
+ overlap,
2747
+ atoms,
2748
+ scopes,
2749
+ }),
2750
+ };
2751
+ }
2752
+
2460
2753
  async function captureReviewPackContext(
2461
2754
  browser: Browser,
2462
2755
  targetArg: string,
@@ -2465,120 +2758,125 @@ async function captureReviewPackContext(
2465
2758
  qaCapture: QaCaptureState | undefined,
2466
2759
  ): Promise<ReviewPackReport> {
2467
2760
  const packDir = opts.reviewPack as string;
2468
- const { maxTiles, band, overlap } = critiqueTilingKnobs(opts);
2469
- const buffer = await browser.fullPageScreenshotBuffer();
2470
- let atoms: Awaited<ReturnType<Browser["visualAtoms"]>> | undefined;
2471
- try {
2472
- atoms = await browser.visualAtoms();
2473
- } catch {
2474
- atoms = undefined;
2475
- }
2476
- const tiling = { bandHeight: band, overlap, maxTiles, atoms };
2477
- const withBands = opts.reviewPackBands !== false;
2478
- // Gate-hit rectangles (qa-run lifts them from the gate stage, which runs
2479
- // first) pull the bands they land in past the cap; those tiles follow the
2480
- // kept bands as `hit band N`. Coverage still describes the kept prefix.
2481
- const hitRects = parseReviewPackHitRects(opts.reviewPackHitRect);
2482
- const banded = withBands
2483
- ? tilesFromFullPage(buffer, { ...tiling, hitRects, maxHitBands: REVIEW_PACK_HIT_RECT_MAX })
2484
- : undefined;
2485
- if (banded?.coverage.capped) {
2486
- emit.log(
2487
- `review-pack: page banded to the ${maxTiles}-tile cap (raise --check-critique-max-tiles for full coverage)`,
2488
- "warn",
2489
- );
2490
- }
2491
- if (banded && banded.hitBands > 0) {
2492
- emit.log(
2493
- `review-pack: ${banded.hitBands} band(s) below the tile cap captured for ${hitRects.length} gate-hit rect(s)`,
2494
- "info",
2495
- );
2496
- }
2497
- // Fidelity: prove the full-page screenshot against scrolled viewport
2498
- // renders while the browser is still open; on disagreement the bands are
2499
- // re-cut from scrolled captures (see reconcileCaptureFidelity).
2500
- let bandTiles = banded?.tiles ?? [];
2501
- let captureFidelity: PageReviewCaptureFidelity | undefined;
2502
- const fidelityWarnings: string[] = [];
2503
- if (bandTiles.length > 0) {
2504
- const reconciled = await reconcileCaptureFidelity(browser, bandTiles);
2505
- bandTiles = reconciled.tiles;
2506
- captureFidelity = reconciled.fidelity;
2507
- fidelityWarnings.push(...reconciled.warnings);
2508
- for (const warning of reconciled.warnings) emit.log(warning, "warn");
2509
- emit.log(
2510
- `review-pack: capture fidelity ${captureFidelity.source} ` +
2511
- `(${captureFidelity.probed.length} band(s) probed, ${captureFidelity.mismatched.length} mismatched)`,
2512
- "info",
2513
- );
2514
- }
2515
- const scopeTiles: Array<{ selector: string; tiles: CritiqueTile[]; coverage: CritiqueCoverage }> =
2516
- [];
2517
- for (const selector of opts.reviewPackScope ?? []) {
2518
- const elementRects = await browser.elementTiles(selector);
2519
- const scoped = tilesFromFullPage(buffer, { ...tiling, elementRects });
2520
- if (elementRects.length > maxTiles) {
2521
- emit.log(
2522
- `review-pack: ${elementRects.length} elements matched ${selector}, capped to ${maxTiles} tiles`,
2523
- "warn",
2761
+ const plan = await buildReviewCapturePlan(browser, opts);
2762
+ let allocation: PageReviewContextAllocation;
2763
+ if (opts.captureTransactionAllocation) {
2764
+ try {
2765
+ allocation = validatePageReviewAllocation(opts.captureTransactionAllocation, plan);
2766
+ } catch (error) {
2767
+ throw new CaptureSourceChanged(String(error));
2768
+ }
2769
+ } else if (opts.reviewPackAllocation) {
2770
+ try {
2771
+ allocation = validatePageReviewAllocation(
2772
+ JSON.parse(readFileSync(resolve(opts.reviewPackAllocation), "utf8")),
2773
+ plan,
2524
2774
  );
2775
+ } catch (error) {
2776
+ if (error instanceof Error && opts.captureSourceEvidence)
2777
+ error.message += ` Current source identity: ${opts.captureSourceEvidence.path} (sha256 ${plan.source_digest}).`;
2778
+ throw error;
2525
2779
  }
2526
- scopeTiles.push({ selector, tiles: scoped.tiles, coverage: scoped.coverage });
2527
- }
2528
- // Coverage: the bands when present, else the scopes folded together
2529
- // (heights worst-case, band counts add, capped sticky).
2530
- let coverage: CritiqueCoverage | undefined = banded?.coverage;
2531
- if (!coverage) {
2532
- for (const scope of scopeTiles) {
2533
- coverage = coverage
2534
- ? {
2535
- page_height_px: Math.max(coverage.page_height_px, scope.coverage.page_height_px),
2536
- reviewed_height_px: Math.min(
2537
- coverage.reviewed_height_px,
2538
- scope.coverage.reviewed_height_px,
2539
- ),
2540
- bands_total: coverage.bands_total + scope.coverage.bands_total,
2541
- bands_reviewed: coverage.bands_reviewed + scope.coverage.bands_reviewed,
2542
- capped: coverage.capped || scope.coverage.capped,
2543
- }
2544
- : scope.coverage;
2780
+ } else {
2781
+ // Standalone capture retains its 24-tile default; it still obeys one hard total.
2782
+ const maxTiles = critiqueTilingKnobs(opts).maxTiles;
2783
+ const hits = parseReviewPackHitRects(opts.reviewPackHitRect);
2784
+ for (const c of plan.candidates) {
2785
+ c.gate_hits = hits.flatMap((h, i) =>
2786
+ c.rect.x < (h.x + h.width) * plan.dpr &&
2787
+ c.rect.x + c.rect.width > h.x * plan.dpr &&
2788
+ c.rect.y < (h.y + h.height) * plan.dpr &&
2789
+ c.rect.y + c.rect.height > h.y * plan.dpr
2790
+ ? [{ check_id: `hit:${i + 1}`, severity: "high" as const }]
2791
+ : [],
2792
+ );
2545
2793
  }
2794
+ allocation = allocateTileBudget([plan], maxTiles).contexts[0];
2546
2795
  }
2547
- const page = await browser.currentPage.evaluate(() => ({
2548
- width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
2549
- height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
2796
+ const start = performance.now();
2797
+ const buffer = await browser.fullPageScreenshotBuffer();
2798
+ const size = pngDimensions(buffer);
2799
+ if (size.width !== plan.page_width || size.height !== plan.page_height)
2800
+ throw opts.captureTransactionAllocation
2801
+ ? new CaptureSourceChanged("Page review image geometry changed after planning.")
2802
+ : new Error("Page review image geometry changed after planning; rerun the review.");
2803
+ const decoded = PNG.sync.read(buffer);
2804
+ const fullPageMs = performance.now() - start;
2805
+ const byId = new Map(plan.candidates.map((c) => [c.id, c]));
2806
+ const selected = allocation.selected_ids.map((id) => byId.get(id)!);
2807
+ const cropStart = performance.now();
2808
+ const tiles: CritiqueTile[] = selected.map((c, index) => ({
2809
+ index,
2810
+ label: c.label,
2811
+ x: c.rect.x,
2812
+ scrollY: c.rect.y,
2813
+ width: c.rect.width,
2814
+ height: c.rect.height,
2815
+ pngBase64: cropNativePng(decoded, c.rect).toString("base64"),
2550
2816
  }));
2817
+ const cropMs = performance.now() - cropStart;
2818
+ const probeStart = performance.now();
2819
+ const reconciled = await reconcileCaptureFidelity(browser, tiles);
2820
+ const probeMs = performance.now() - probeStart;
2821
+ // A lazy image can change the document bottom while a native probe is running.
2822
+ // Revalidate even failed probes so a changed plan gets the same bounded retry.
2823
+ if (opts.captureTransactionAllocation) {
2824
+ const after = await buildReviewCapturePlan(browser, opts);
2825
+ try {
2826
+ validatePageReviewAllocation(allocation, after);
2827
+ } catch (error) {
2828
+ throw new CaptureSourceChanged(String(error));
2829
+ }
2830
+ }
2831
+ if (reconciled.fidelity.probed.length !== tiles.length)
2832
+ throw new Error(
2833
+ "Page review native fidelity probe incomplete; no context was saved. " +
2834
+ reconciled.warnings.join("; "),
2835
+ );
2836
+ const bandTiles: CritiqueTile[] = [];
2837
+ const scopes = new Map<string, CritiqueTile[]>();
2838
+ for (const [index, c] of selected.entries()) {
2839
+ const tile = reconciled.tiles[index];
2840
+ if (!c.scope) bandTiles.push(tile);
2841
+ else {
2842
+ const group = scopes.get(c.scope) ?? [];
2843
+ group.push(tile);
2844
+ scopes.set(c.scope, group);
2845
+ }
2846
+ }
2847
+
2551
2848
  const capture = qaCapture ?? (await captureQaState(browser, targetArg, navResult.url, opts));
2552
- const record = writePackContext(packDir, {
2553
- context: {
2554
- ...capture.context,
2555
- ...(opts.reviewPackContext ? { id: opts.reviewPackContext } : {}),
2556
- },
2557
- url: navResult.url,
2558
- title: navResult.title,
2559
- fullPage: buffer,
2560
- pageWidth: page.width,
2561
- pageHeight: page.height,
2562
- tiles: bandTiles,
2563
- coverage: coverage ?? {
2564
- page_height_px: page.height,
2565
- reviewed_height_px: 0,
2566
- bands_total: 0,
2567
- bands_reviewed: 0,
2568
- capped: false,
2569
- },
2570
- scopeTiles: scopeTiles.map(({ selector, tiles }) => ({ selector, tiles })),
2571
- signature: capture.signature,
2572
- domHtml: capture.domHtml,
2573
- ...(captureFidelity ? { captureFidelity } : {}),
2574
- hitBands: banded?.hitBands ?? 0,
2575
- });
2849
+ const writeContext = (destination: string) =>
2850
+ writePackContext(destination, {
2851
+ context: { ...capture.context, id: plan.context_id },
2852
+ url: navResult.url,
2853
+ title: navResult.title,
2854
+ fullPage: buffer,
2855
+ pageWidth: plan.page_width / plan.dpr,
2856
+ pageHeight: plan.page_height / plan.dpr,
2857
+ tiles: bandTiles,
2858
+ coverage: allocation.coverage,
2859
+ scopeTiles: [...scopes].map(([selector, tiles]) => ({ selector, tiles })),
2860
+ signature: capture.signature,
2861
+ domHtml: capture.domHtml,
2862
+ captureFidelity: reconciled.fidelity,
2863
+ viewportSize: { width: plan.viewport_width, height: plan.viewport_height },
2864
+ dpr: plan.dpr,
2865
+ recipeVersion: plan.recipe_version,
2866
+ allocationCoverage: allocation.coverage,
2867
+ capturePlan: allocation.plan,
2868
+ hitBands: 0,
2869
+ });
2870
+ const record = opts.captureTransactionAllocation
2871
+ ? writeCaptureTransaction(packDir, plan.context_id, writeContext)
2872
+ : writeContext(packDir);
2873
+ const warnings = [...reconciled.warnings];
2874
+ if (allocation.coverage.capped)
2875
+ warnings.push(
2876
+ `Review coverage is incomplete: ${allocation.coverage.uncovered_intervals.length} uncovered intervals and ${allocation.coverage.omitted_scopes.length} omitted scopes.`,
2877
+ );
2576
2878
  emit.log(
2577
- `review-pack: ${record.id} → ${record.tiles.length} tile(s)` +
2578
- (record.scopes.length > 0
2579
- ? ` (${record.scopes.map((s) => `${s.selector}: ${s.tiles}`).join(", ")})`
2580
- : "") +
2581
- ` written to ${packDir}`,
2879
+ `review-pack: ${record.id} captured ${record.tiles.length} tiles; full-page ${Math.round(fullPageMs)} ms, crop ${Math.round(cropMs)} ms, native probe ${Math.round(probeMs)} ms`,
2582
2880
  "info",
2583
2881
  );
2584
2882
  return {
@@ -2588,9 +2886,8 @@ async function captureReviewPackContext(
2588
2886
  scopes: record.scopes,
2589
2887
  coverage: record.coverage,
2590
2888
  files: record.files,
2591
- ...(record.hit_bands !== undefined ? { hit_bands: record.hit_bands } : {}),
2592
- ...(record.capture_fidelity ? { capture_fidelity: record.capture_fidelity } : {}),
2593
- ...(fidelityWarnings.length > 0 ? { warnings: fidelityWarnings } : {}),
2889
+ capture_fidelity: reconciled.fidelity,
2890
+ ...(warnings.length ? { warnings } : {}),
2594
2891
  };
2595
2892
  }
2596
2893
 
@@ -2735,7 +3032,7 @@ async function waitForTerminalEnter(signal: AbortSignal): Promise<void> {
2735
3032
  });
2736
3033
  }
2737
3034
 
2738
- /** Upper bound on --review-pack-hit-rect values (and on hit bands cut past the cap). */
3035
+ /** Upper bound on manually supplied --review-pack-hit-rect values. */
2739
3036
  const REVIEW_PACK_HIT_RECT_MAX = 50;
2740
3037
 
2741
3038
  /** `x,y,w,h` integer rectangles from repeated --review-pack-hit-rect flags. */