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
@@ -12,9 +12,12 @@
12
12
  // Storage is content-under-a-key, like visual-diff baselines: a directory per
13
13
  // target, a subdirectory per context, atomic writes (tmp + rename) so a
14
14
  // crashed writer never leaves a half-baseline the next run would trust.
15
+ import { randomUUID } from "node:crypto";
15
16
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
16
17
  import { homedir } from "node:os";
17
18
  import { resolve } from "node:path";
19
+ import { PAGE_REVIEW_DECISIONS_SCHEMA, } from "./page-review-contracts.js";
20
+ import { machineFindingKey, validateNativeEvidence } from "./page-review-evidence.js";
18
21
  import { fnv1a32 } from "./qa-plan.js";
19
22
  const SNAPSHOT_DIR = resolve(homedir(), ".cache", "harnery", "qa-snapshots");
20
23
  /** Filesystem-safe key for a target URL/file + render context. The digest
@@ -47,72 +50,280 @@ function contextDirName(context) {
47
50
  function snapshotDir(target, context, opts) {
48
51
  return resolve(opts.root ?? SNAPSHOT_DIR, qaSnapshotKey(target), contextDirName(context));
49
52
  }
53
+ /** Serialize both owned slots across processes. A stuck writer fails closed. */
54
+ function withStoreLock(dir, operation) {
55
+ mkdirSync(dir, { recursive: true });
56
+ const lock = resolve(dir, ".write-lock");
57
+ const deadline = Date.now() + 5_000;
58
+ while (true) {
59
+ try {
60
+ mkdirSync(lock);
61
+ break;
62
+ }
63
+ catch (error) {
64
+ if (error.code !== "EEXIST")
65
+ throw error;
66
+ if (Date.now() >= deadline)
67
+ throw new Error(`Snapshot writer lock busy: ${dir}`);
68
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
69
+ }
70
+ }
71
+ try {
72
+ return operation();
73
+ }
74
+ finally {
75
+ rmSync(lock, { recursive: true, force: true });
76
+ }
77
+ }
78
+ function validateDecisionSlot(target, context, slot) {
79
+ const only = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
80
+ if (slot.schema !== PAGE_REVIEW_DECISIONS_SCHEMA ||
81
+ slot.complete !== true ||
82
+ slot.target !== target ||
83
+ slot.context.viewport !== context.viewport ||
84
+ slot.context.theme !== context.theme ||
85
+ slot.context.state !== context.state ||
86
+ slot.context.critique_contract_version !== 2 ||
87
+ ![slot.context.viewport_width, slot.context.viewport_height, slot.context.dpr].every((n) => Number.isFinite(n) && n > 0) ||
88
+ !/^[a-f0-9]{64}$/.test(slot.context.rubric_digest) ||
89
+ !slot.context.recipe_version ||
90
+ !only(slot, [
91
+ "schema",
92
+ "complete",
93
+ "target",
94
+ "context",
95
+ "source_run",
96
+ "source_revision",
97
+ "reviewer",
98
+ "reviewed_at",
99
+ "decisions",
100
+ ]) ||
101
+ !only(slot.context, [
102
+ "viewport",
103
+ "viewport_width",
104
+ "viewport_height",
105
+ "theme",
106
+ "state",
107
+ "dpr",
108
+ "recipe_version",
109
+ "rubric_digest",
110
+ "critique_contract_version",
111
+ ]) ||
112
+ !slot.reviewer?.trim() ||
113
+ !Number.isFinite(Date.parse(slot.reviewed_at)) ||
114
+ !slot.source_run ||
115
+ !Array.isArray(slot.decisions))
116
+ throw new Error("Invalid durable review decision slot");
117
+ const keys = new Set();
118
+ for (const decision of slot.decisions) {
119
+ if (!decision.reviewer?.trim() ||
120
+ !Number.isFinite(Date.parse(decision.at)) ||
121
+ keys.has(decision.finding_key) ||
122
+ !/^[a-f0-9]{64}$/.test(decision.tile_pixel_digest) ||
123
+ !only(decision, [
124
+ "finding_key",
125
+ "tile_pixel_digest",
126
+ "rect",
127
+ "finding",
128
+ "disposition",
129
+ "reviewer",
130
+ "at",
131
+ "note",
132
+ ]) ||
133
+ !only(decision.finding, ["severity", "category", "description"]) ||
134
+ !only(decision.rect, ["x", "y", "width", "height"]) ||
135
+ ![decision.rect.x, decision.rect.y, decision.rect.width, decision.rect.height].every(Number.isInteger) ||
136
+ decision.rect.x < 0 ||
137
+ decision.rect.y < 0 ||
138
+ decision.rect.width < 1 ||
139
+ decision.rect.height < 1 ||
140
+ !["artifact", "not-a-defect", "confirmed", "duplicate-of-gate"].includes(decision.disposition) ||
141
+ decision.finding.category === "provider-error" ||
142
+ machineFindingKey(slot.context, { rect: decision.rect, pixel_digest: decision.tile_pixel_digest }, decision.finding) !== decision.finding_key) {
143
+ throw new Error("Invalid durable review finding identity");
144
+ }
145
+ keys.add(decision.finding_key);
146
+ }
147
+ }
148
+ export function loadReviewDecisions(target, context, opts = {}) {
149
+ const dir = snapshotDir(target, context, opts);
150
+ if (!existsSync(dir))
151
+ return null;
152
+ try {
153
+ return withStoreLock(dir, () => {
154
+ const slot = JSON.parse(readFileSync(resolve(dir, "review-decisions.json"), "utf8"));
155
+ validateDecisionSlot(target, context, slot);
156
+ return slot;
157
+ });
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ /** Decisions contain text and digests only; replacing a baseline cannot touch this slot. */
164
+ export function saveReviewDecisions(target, context, slot, opts = {}) {
165
+ validateDecisionSlot(target, context, slot);
166
+ const dir = snapshotDir(target, context, opts);
167
+ withStoreLock(dir, () => {
168
+ const path = resolve(dir, "review-decisions.json");
169
+ const temp = `${path}.tmp-${randomUUID()}`;
170
+ try {
171
+ writeFileSync(temp, `${JSON.stringify(slot)}\n`);
172
+ renameSync(temp, path);
173
+ }
174
+ finally {
175
+ rmSync(temp, { force: true });
176
+ }
177
+ });
178
+ }
50
179
  /**
51
180
  * Persist a QA snapshot atomically. The whole context directory is staged as
52
181
  * `<dir>.tmp-<pid>` and renamed into place, replacing any prior snapshot for
53
182
  * the same target + context — a reader never observes a partial write.
54
183
  */
55
184
  export function saveQaSnapshot(target, context, content, opts = {}) {
56
- const dir = snapshotDir(target, context, opts);
57
- const staging = `${dir}.tmp-${process.pid}`;
58
- rmSync(staging, { recursive: true, force: true });
59
- mkdirSync(staging, { recursive: true });
60
- writeFileSync(resolve(staging, "signature.json"), `${JSON.stringify(content.signature)}\n`);
61
- if (content.domHtml !== undefined) {
62
- writeFileSync(resolve(staging, "dom.html"), content.domHtml);
63
- }
64
- if (content.screenshotPng !== undefined) {
65
- writeFileSync(resolve(staging, "screenshot.png"), content.screenshotPng);
66
- }
67
- if (content.critique !== undefined) {
68
- writeFileSync(resolve(staging, "critique.json"), `${JSON.stringify(content.critique)}\n`);
69
- }
70
- rmSync(dir, { recursive: true, force: true });
71
- renameSync(staging, dir);
72
- return {
73
- signature: content.signature,
74
- path: dir,
75
- ...(content.domHtml !== undefined ? { domHtmlPath: resolve(dir, "dom.html") } : {}),
76
- ...(content.screenshotPng !== undefined
77
- ? { screenshotPath: resolve(dir, "screenshot.png") }
78
- : {}),
79
- };
185
+ const container = snapshotDir(target, context, opts);
186
+ return withStoreLock(container, () => {
187
+ const dir = resolve(container, "baseline");
188
+ const staging = `${dir}.tmp-${randomUUID()}`;
189
+ const previous = `${dir}.previous`;
190
+ if (content.tileEvidence && content.screenshotPng)
191
+ throw new Error("A pack native-tile baseline must not retain a full-page image");
192
+ if (content.tileEvidence && !validateNativeEvidence(content.tileEvidence))
193
+ throw new Error("Invalid native tile baseline evidence");
194
+ mkdirSync(staging, { recursive: true });
195
+ try {
196
+ writeFileSync(resolve(staging, "signature.json"), `${JSON.stringify(content.signature)}\n`);
197
+ if (content.domHtml !== undefined) {
198
+ writeFileSync(resolve(staging, "dom.html"), content.domHtml);
199
+ }
200
+ if (content.screenshotPng !== undefined) {
201
+ writeFileSync(resolve(staging, "screenshot.png"), content.screenshotPng);
202
+ }
203
+ if (content.critique !== undefined) {
204
+ writeFileSync(resolve(staging, "critique.json"), `${JSON.stringify(content.critique)}\n`);
205
+ }
206
+ if (content.tileEvidence) {
207
+ const tiles = content.tileEvidence.tiles.map(({ png, ...tile }, i) => {
208
+ const file = `tile-${i}.png`;
209
+ writeFileSync(resolve(staging, file), png);
210
+ return { ...tile, file };
211
+ });
212
+ writeFileSync(resolve(staging, "native-evidence.json"), JSON.stringify({ ...content.tileEvidence, tiles }));
213
+ }
214
+ // Recovery of a writer interrupted after moving its old baseline.
215
+ if (!existsSync(dir) && existsSync(previous))
216
+ renameSync(previous, dir);
217
+ rmSync(previous, { recursive: true, force: true });
218
+ if (existsSync(dir))
219
+ renameSync(dir, previous);
220
+ try {
221
+ renameSync(staging, dir);
222
+ }
223
+ catch (error) {
224
+ if (existsSync(previous))
225
+ renameSync(previous, dir);
226
+ throw error;
227
+ }
228
+ rmSync(previous, { recursive: true, force: true });
229
+ // Remove superseded legacy baseline files, never the independent decision slot.
230
+ for (const name of [
231
+ "signature.json",
232
+ "dom.html",
233
+ "screenshot.png",
234
+ "critique.json",
235
+ "native-evidence.json",
236
+ ]) {
237
+ rmSync(resolve(container, name), { force: true });
238
+ }
239
+ return {
240
+ signature: content.signature,
241
+ path: dir,
242
+ ...(content.domHtml !== undefined ? { domHtmlPath: resolve(dir, "dom.html") } : {}),
243
+ ...(content.screenshotPng !== undefined
244
+ ? { screenshotPath: resolve(dir, "screenshot.png") }
245
+ : {}),
246
+ ...(content.critique ? { critique: content.critique } : {}),
247
+ ...(content.tileEvidence ? { tileEvidence: content.tileEvidence } : {}),
248
+ };
249
+ }
250
+ finally {
251
+ rmSync(staging, { recursive: true, force: true });
252
+ }
253
+ });
80
254
  }
81
255
  /** Load the persisted snapshot for a target + context, or null. A corrupt
82
256
  * signature file returns null (and the caller re-acquires a baseline) rather
83
257
  * than throwing — a bad cache entry must never block QA. */
84
258
  export function loadQaSnapshot(target, context, opts = {}) {
85
- const dir = snapshotDir(target, context, opts);
86
- const sigPath = resolve(dir, "signature.json");
87
- if (!existsSync(sigPath))
259
+ const container = snapshotDir(target, context, opts);
260
+ if (!existsSync(container))
88
261
  return null;
89
262
  try {
90
- const signature = JSON.parse(readFileSync(sigPath, "utf8"));
91
- if (!signature || !Array.isArray(signature.nodes) || !Array.isArray(signature.stylesheets)) {
92
- return null;
93
- }
94
- const domHtmlPath = resolve(dir, "dom.html");
95
- const screenshotPath = resolve(dir, "screenshot.png");
96
- const critiquePath = resolve(dir, "critique.json");
97
- let critique;
98
- if (existsSync(critiquePath)) {
263
+ return withStoreLock(container, () => {
264
+ const baseline = resolve(container, "baseline");
265
+ const dir = existsSync(baseline)
266
+ ? baseline
267
+ : existsSync(`${baseline}.previous`)
268
+ ? `${baseline}.previous`
269
+ : container;
270
+ const sigPath = resolve(dir, "signature.json");
271
+ if (!existsSync(sigPath))
272
+ return null;
99
273
  try {
100
- const parsed = JSON.parse(readFileSync(critiquePath, "utf8"));
101
- if (parsed && typeof parsed.contract_version === "number" && Array.isArray(parsed.tiles)) {
102
- critique = parsed;
274
+ const signature = JSON.parse(readFileSync(sigPath, "utf8"));
275
+ if (!signature ||
276
+ !Array.isArray(signature.nodes) ||
277
+ !Array.isArray(signature.stylesheets)) {
278
+ return null;
279
+ }
280
+ const domHtmlPath = resolve(dir, "dom.html");
281
+ const screenshotPath = resolve(dir, "screenshot.png");
282
+ const critiquePath = resolve(dir, "critique.json");
283
+ let critique;
284
+ let tileEvidence;
285
+ try {
286
+ const raw = JSON.parse(readFileSync(resolve(dir, "native-evidence.json"), "utf8"));
287
+ const candidate = {
288
+ ...raw,
289
+ tiles: raw.tiles.map((tile) => {
290
+ if (!/^tile-\d+\.png$/.test(tile.file))
291
+ throw new Error("Invalid native tile path");
292
+ return { ...tile, png: readFileSync(resolve(dir, tile.file)) };
293
+ }),
294
+ };
295
+ if (validateNativeEvidence(candidate))
296
+ tileEvidence = candidate;
297
+ }
298
+ catch {
299
+ /* Missing or corrupt evidence is a cache miss. */
103
300
  }
301
+ if (existsSync(critiquePath)) {
302
+ try {
303
+ const parsed = JSON.parse(readFileSync(critiquePath, "utf8"));
304
+ if (parsed &&
305
+ typeof parsed.contract_version === "number" &&
306
+ Array.isArray(parsed.tiles)) {
307
+ critique = parsed;
308
+ }
309
+ }
310
+ catch {
311
+ // corrupt critique.json degrades to a reuse miss, never an error
312
+ }
313
+ }
314
+ return {
315
+ signature,
316
+ path: dir,
317
+ ...(existsSync(domHtmlPath) ? { domHtmlPath } : {}),
318
+ ...(existsSync(screenshotPath) ? { screenshotPath } : {}),
319
+ ...(critique ? { critique } : {}),
320
+ ...(tileEvidence ? { tileEvidence } : {}),
321
+ };
104
322
  }
105
323
  catch {
106
- // corrupt critique.json degrades to a reuse miss, never an error
324
+ return null;
107
325
  }
108
- }
109
- return {
110
- signature,
111
- path: dir,
112
- ...(existsSync(domHtmlPath) ? { domHtmlPath } : {}),
113
- ...(existsSync(screenshotPath) ? { screenshotPath } : {}),
114
- ...(critique ? { critique } : {}),
115
- };
326
+ });
116
327
  }
117
328
  catch {
118
329
  return null;
@@ -0,0 +1,13 @@
1
+ import type { PageReviewCapturePlan, PageReviewContextAllocation } from "./page-review-contracts.js";
2
+ /** Publish a complete context atomically; failed writes stay outside discoverable contexts. */
3
+ export declare function writeCaptureTransaction<T>(packDir: string, contextId: string, write: (stage: string) => T): T;
4
+ /** A preliminary allocation reserves pixels; it never supplies current source evidence. */
5
+ export declare function allocateCaptureReservation(reservation: PageReviewContextAllocation, current: PageReviewCapturePlan): PageReviewContextAllocation;
6
+ export declare class CaptureSourceChanged extends Error {
7
+ constructor(message?: string);
8
+ }
9
+ /** Gate-hit annotations may change, but source and every native rectangle must stay fixed. */
10
+ export declare function assertGatePlanStable(before: PageReviewCapturePlan, after: PageReviewCapturePlan): void;
11
+ /** One retry recollects every gate and allocation; no previous green result survives. */
12
+ export declare function runCaptureTransaction<T>(collectAndAllocate: (attempt: number) => Promise<void>, capture: () => Promise<T>, recordFailure: (attempt: number, error: unknown) => void): Promise<T>;
13
+ //# sourceMappingURL=review-capture-transaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-capture-transaction.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/review-capture-transaction.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,qBAAqB,EACrB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAGpC,+FAA+F;AAC/F,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,GAC1B,CAAC,CAgBH;AAED,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,2BAA2B,EACxC,OAAO,EAAE,qBAAqB,GAC7B,2BAA2B,CA6B7B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,SAAiE;CAIrF;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,qBAAqB,GAC3B,IAAI,CAKN;AAED,yFAAyF;AACzF,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EACtD,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACvD,OAAO,CAAC,CAAC,CAAC,CAWZ"}
@@ -0,0 +1,79 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { allocateTileBudget, capturePlanDigest, validatePageReviewAllocation, } from "./page-review-budget.js";
4
+ import { packPaths } from "./page-review-pack.js";
5
+ /** Publish a complete context atomically; failed writes stay outside discoverable contexts. */
6
+ export function writeCaptureTransaction(packDir, contextId, write) {
7
+ mkdirSync(packDir, { recursive: true, mode: 0o700 });
8
+ const stage = mkdtempSync(join(packDir, ".pending-capture-"));
9
+ try {
10
+ const result = write(stage);
11
+ const destination = packPaths(packDir).contextDir(contextId);
12
+ mkdirSync(dirname(destination), { recursive: true });
13
+ if (existsSync(destination))
14
+ throw new Error("Capture context already exists; refusing to replace prior evidence.");
15
+ renameSync(packPaths(stage).contextDir(contextId), destination);
16
+ rmSync(stage, { recursive: true });
17
+ return result;
18
+ }
19
+ catch (error) {
20
+ if (error instanceof Error)
21
+ error.message += ` Incomplete capture retained at ${stage}.`;
22
+ throw error;
23
+ }
24
+ }
25
+ /** A preliminary allocation reserves pixels; it never supplies current source evidence. */
26
+ export function allocateCaptureReservation(reservation, current) {
27
+ validatePageReviewAllocation(reservation, reservation.plan);
28
+ for (const key of [
29
+ "context_id",
30
+ "viewport",
31
+ "viewport_width",
32
+ "viewport_height",
33
+ "theme",
34
+ "state",
35
+ "dpr",
36
+ "recipe_version",
37
+ ]) {
38
+ if (reservation.plan[key] !== current[key])
39
+ throw new Error(`Capture reservation changed ${key}.`);
40
+ }
41
+ if (reservation.plan.required_scopes.some((scope) => !current.required_scopes.includes(scope)))
42
+ throw new Error("Capture reservation lost a required scope.");
43
+ // Reservations sum to the original global allocation, even when its per-context
44
+ // ceiling was higher than the number of tiles actually assigned to that context.
45
+ const ceiling = reservation.selected_ids.length;
46
+ const allocation = allocateTileBudget([current], ceiling, { [current.context_id]: ceiling })
47
+ .contexts[0];
48
+ if (allocation.coverage.capped ||
49
+ allocation.coverage.omitted_scopes.length ||
50
+ allocation.coverage.uncovered_intervals.length)
51
+ throw new Error("Fresh native capture coverage exceeds the reserved tile budget.");
52
+ return validatePageReviewAllocation(allocation, current);
53
+ }
54
+ export class CaptureSourceChanged extends Error {
55
+ constructor(message = "Page source changed during the gate and capture transaction.") {
56
+ super(message);
57
+ this.name = "CaptureSourceChanged";
58
+ }
59
+ }
60
+ /** Gate-hit annotations may change, but source and every native rectangle must stay fixed. */
61
+ export function assertGatePlanStable(before, after) {
62
+ if (capturePlanDigest(before) !== capturePlanDigest(after))
63
+ throw new CaptureSourceChanged("Page source or geometry changed while deterministic gates were running.");
64
+ }
65
+ /** One retry recollects every gate and allocation; no previous green result survives. */
66
+ export async function runCaptureTransaction(collectAndAllocate, capture, recordFailure) {
67
+ for (let attempt = 1; attempt <= 2; attempt++) {
68
+ try {
69
+ await collectAndAllocate(attempt);
70
+ return await capture();
71
+ }
72
+ catch (error) {
73
+ recordFailure(attempt, error);
74
+ if (!(error instanceof CaptureSourceChanged) || attempt === 2)
75
+ throw error;
76
+ }
77
+ }
78
+ throw new Error("Capture transaction did not complete.");
79
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"standalone-html.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/standalone-html.ts"],"names":[],"mappings":"AAmBA,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAkB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,CAC3C,IAAI,EAAE,qBAAqB,KACxB,OAAO,CAAC,oBAAoB,CAAC,CAuQjC"}
1
+ {"version":3,"file":"standalone-html.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/standalone-html.ts"],"names":[],"mappings":"AAmBA,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAkB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,CAC3C,IAAI,EAAE,qBAAqB,KACxB,OAAO,CAAC,oBAAoB,CAAC,CAyQjC"}
@@ -45,7 +45,9 @@ export function buildStandaloneHtmlScript() {
45
45
  return cached;
46
46
  let result = null;
47
47
  try {
48
- const res = await fetch(url, { credentials: "include" });
48
+ // Retain access to authenticated same-origin assets without imposing
49
+ // credentialed CORS on public assets served by another origin.
50
+ const res = await fetch(url, { credentials: "same-origin" });
49
51
  if (res.ok) {
50
52
  const buf = await res.arrayBuffer();
51
53
  const fits = buf.byteLength <= maxResourceBytes &&
@@ -137,7 +139,7 @@ export function buildStandaloneHtmlScript() {
137
139
  }
138
140
  let css = null;
139
141
  try {
140
- const res = await fetch(url, { credentials: "include" });
142
+ const res = await fetch(url, { credentials: "same-origin" });
141
143
  if (res.ok)
142
144
  css = await res.text();
143
145
  }
@@ -0,0 +1,34 @@
1
+ export interface CaptureFingerprint {
2
+ version: string;
3
+ sha256: string;
4
+ }
5
+ export interface CaptureThumbnailAssociation {
6
+ schema_version: 2;
7
+ producer: "browser-standalone-capture";
8
+ source: CaptureFingerprint & {
9
+ path: string;
10
+ };
11
+ preview: CaptureFingerprint & {
12
+ path: string;
13
+ };
14
+ }
15
+ /** Take immediately after the screenshot writer completes, before asynchronous
16
+ * HTML serialization. A later writer cannot silently substitute its PNG. */
17
+ export declare function captureThumbnailFingerprint(pngPath: string): CaptureFingerprint | null;
18
+ /** Associate only the full-document, standalone HTML and exact PNG produced by
19
+ * this capture operation. No basename inference and no reads of source pages.
20
+ * A linked asset, selector fragment, changed output, oversize file or output
21
+ * outside a managed artifact workspace safely skips registration.
22
+ * The hidden sidecar binds canonical repository paths, checked file versions
23
+ * and SHA256 receipts. Web readers also require an empty local asset graph;
24
+ * manual registrations bind their own complete dependency graph instead. */
25
+ export declare function registerCaptureThumbnail(input: {
26
+ htmlPath: string;
27
+ pngPath: string;
28
+ html: string;
29
+ preview: CaptureFingerprint;
30
+ fullDocument: boolean;
31
+ stylesheetsLinked: number;
32
+ resourcesLinked: number;
33
+ }): string | null;
34
+ //# sourceMappingURL=thumbnail-association.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail-association.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/thumbnail-association.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,2BAA2B;IAC1C,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,4BAA4B,CAAC;IACvC,MAAM,EAAE,kBAAkB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,OAAO,EAAE,kBAAkB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AA8BD;4EAC4E;AAC5E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAMtF;AAwBD;;;;;;4EAM4E;AAC5E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,GAAG,IAAI,CAsFhB"}
@@ -0,0 +1,158 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
3
+ import path from "node:path";
4
+ const MAX_HTML = 2 * 1024 * 1024;
5
+ const MAX_PNG = 32 * 1024 * 1024;
6
+ function version(fd) {
7
+ const s = fstatSync(fd);
8
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}`;
9
+ }
10
+ function fingerprint(filename, maxBytes) {
11
+ const canonical = realpathSync(filename);
12
+ const before = statSync(canonical);
13
+ if (!before.isFile() || before.size > maxBytes)
14
+ return null;
15
+ const fd = openSync(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
16
+ try {
17
+ const opened = fstatSync(fd);
18
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino)
19
+ return null;
20
+ const initial = version(fd);
21
+ const hash = createHash("sha256");
22
+ const bytes = Buffer.alloc(64 * 1024);
23
+ let offset = 0;
24
+ while (offset < before.size) {
25
+ const count = readSync(fd, bytes, 0, Math.min(bytes.length, before.size - offset), offset);
26
+ if (!count)
27
+ return null;
28
+ hash.update(bytes.subarray(0, count));
29
+ offset += count;
30
+ }
31
+ if (version(fd) !== initial || realpathSync(filename) !== canonical)
32
+ return null;
33
+ return { version: initial, sha256: hash.digest("hex") };
34
+ }
35
+ finally {
36
+ closeSync(fd);
37
+ }
38
+ }
39
+ /** Take immediately after the screenshot writer completes, before asynchronous
40
+ * HTML serialization. A later writer cannot silently substitute its PNG. */
41
+ export function captureThumbnailFingerprint(pngPath) {
42
+ try {
43
+ return fingerprint(pngPath, MAX_PNG);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function workspaceFor(filename) {
50
+ for (let current = path.dirname(filename); path.dirname(current) !== current; current = path.dirname(current)) {
51
+ const artifacts = path.dirname(current);
52
+ const state = path.dirname(artifacts);
53
+ if (path.basename(artifacts) === "artifacts" && path.basename(state) === ".harnery")
54
+ return { root: path.dirname(state), workspace: current };
55
+ }
56
+ return null;
57
+ }
58
+ function inside(root, filename) {
59
+ const relative = path.relative(root, filename);
60
+ return (relative !== "" &&
61
+ relative !== ".." &&
62
+ !relative.startsWith(`..${path.sep}`) &&
63
+ !path.isAbsolute(relative));
64
+ }
65
+ /** Associate only the full-document, standalone HTML and exact PNG produced by
66
+ * this capture operation. No basename inference and no reads of source pages.
67
+ * A linked asset, selector fragment, changed output, oversize file or output
68
+ * outside a managed artifact workspace safely skips registration.
69
+ * The hidden sidecar binds canonical repository paths, checked file versions
70
+ * and SHA256 receipts. Web readers also require an empty local asset graph;
71
+ * manual registrations bind their own complete dependency graph instead. */
72
+ export function registerCaptureThumbnail(input) {
73
+ if (!input.fullDocument || input.stylesheetsLinked !== 0 || input.resourcesLinked !== 0)
74
+ return null;
75
+ try {
76
+ const htmlPath = realpathSync(input.htmlPath);
77
+ const pngPath = realpathSync(input.pngPath);
78
+ if (!/\.html?$/i.test(htmlPath) || !/\.png$/i.test(pngPath))
79
+ return null;
80
+ const owner = workspaceFor(htmlPath);
81
+ if (!owner || !inside(owner.workspace, pngPath))
82
+ return null;
83
+ const source = fingerprint(htmlPath, MAX_HTML);
84
+ const preview = fingerprint(pngPath, MAX_PNG);
85
+ if (!source ||
86
+ !preview ||
87
+ source.sha256 !== createHash("sha256").update(input.html).digest("hex") ||
88
+ preview.version !== input.preview.version ||
89
+ preview.sha256 !== input.preview.sha256)
90
+ return null;
91
+ const sourceRelative = path.relative(owner.root, htmlPath).split(path.sep).join("/");
92
+ const previewRelative = path.relative(owner.root, pngPath).split(path.sep).join("/");
93
+ const association = {
94
+ schema_version: 2,
95
+ producer: "browser-standalone-capture",
96
+ source: { path: sourceRelative, ...source },
97
+ preview: { path: previewRelative, ...preview },
98
+ };
99
+ const name = `.thumbnail-preview-${createHash("sha256").update(sourceRelative).digest("hex").slice(0, 24)}.json`;
100
+ const directoryFd = openSync(owner.workspace, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
101
+ try {
102
+ let anchor;
103
+ for (const prefix of ["/proc/self/fd", "/dev/fd"]) {
104
+ const candidate = `${prefix}/${directoryFd}`;
105
+ try {
106
+ if (realpathSync(candidate) === owner.workspace) {
107
+ anchor = candidate;
108
+ break;
109
+ }
110
+ }
111
+ catch {
112
+ /* unsupported descriptor namespace */
113
+ }
114
+ }
115
+ if (!anchor)
116
+ return null;
117
+ const target = path.join(anchor, name);
118
+ const temporary = `${target}.${randomUUID()}.tmp`;
119
+ const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
120
+ try {
121
+ const bytes = Buffer.from(`${JSON.stringify(association)}\n`);
122
+ if (bytes.length > 16 * 1024)
123
+ return null;
124
+ let offset = 0;
125
+ while (offset < bytes.length)
126
+ offset += writeSync(fd, bytes, offset, bytes.length - offset);
127
+ if (realpathSync(anchor) !== owner.workspace ||
128
+ realpathSync(owner.workspace) !== owner.workspace)
129
+ return null;
130
+ // Stat again after the write, without reading another potentially large
131
+ // image. ctime detects same-size edits with a restored mtime.
132
+ const matches = (filename, expected) => {
133
+ const s = statSync(filename);
134
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}` === expected;
135
+ };
136
+ if (!matches(htmlPath, source.version) || !matches(pngPath, preview.version))
137
+ return null;
138
+ renameSync(temporary, target);
139
+ }
140
+ finally {
141
+ closeSync(fd);
142
+ try {
143
+ unlinkSync(temporary);
144
+ }
145
+ catch {
146
+ /* renamed */
147
+ }
148
+ }
149
+ }
150
+ finally {
151
+ closeSync(directoryFd);
152
+ }
153
+ return path.relative(owner.root, path.join(owner.workspace, name)).split(path.sep).join("/");
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }