harnery 0.32.0 → 0.34.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 (665) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -1
  4. package/dist/commands/adapter.js +2 -2
  5. package/dist/commands/agents.d.ts +144 -1
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +1408 -565
  8. package/dist/commands/artifacts.d.ts.map +1 -1
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/backup.d.ts +1 -1
  11. package/dist/commands/backup.d.ts.map +1 -1
  12. package/dist/commands/backup.js +29 -16
  13. package/dist/commands/browse.d.ts.map +1 -1
  14. package/dist/commands/browse.js +234 -12
  15. package/dist/commands/checkpoint.d.ts.map +1 -1
  16. package/dist/commands/checkpoint.js +3 -2
  17. package/dist/commands/claude-desktop.d.ts +2 -2
  18. package/dist/commands/claude-desktop.d.ts.map +1 -1
  19. package/dist/commands/claude-desktop.js +2 -2
  20. package/dist/commands/context.js +1 -6
  21. package/dist/commands/decision.d.ts.map +1 -1
  22. package/dist/commands/decision.js +22 -38
  23. package/dist/commands/docs.d.ts.map +1 -1
  24. package/dist/commands/docs.js +91 -28
  25. package/dist/commands/doctor.d.ts +4 -2
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +55 -7
  28. package/dist/commands/events.d.ts +5 -0
  29. package/dist/commands/events.d.ts.map +1 -0
  30. package/dist/commands/events.js +189 -0
  31. package/dist/commands/governor.d.ts.map +1 -1
  32. package/dist/commands/governor.js +13 -4
  33. package/dist/commands/init.d.ts +10 -2
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +233 -51
  36. package/dist/commands/instructions.d.ts +4 -0
  37. package/dist/commands/instructions.d.ts.map +1 -0
  38. package/dist/commands/instructions.js +100 -0
  39. package/dist/commands/journal.d.ts.map +1 -1
  40. package/dist/commands/journal.js +4 -13
  41. package/dist/commands/ledger-v3.d.ts +5 -0
  42. package/dist/commands/ledger-v3.d.ts.map +1 -0
  43. package/dist/commands/ledger-v3.js +91 -0
  44. package/dist/commands/presence.d.ts.map +1 -1
  45. package/dist/commands/presence.js +15 -6
  46. package/dist/commands/sync.d.ts +1 -1
  47. package/dist/commands/sync.js +1 -1
  48. package/dist/commands/web.d.ts +6 -0
  49. package/dist/commands/web.d.ts.map +1 -1
  50. package/dist/commands/web.js +67 -7
  51. package/dist/commands/work.d.ts.map +1 -1
  52. package/dist/commands/work.js +22 -5
  53. package/dist/core/adapter.d.ts +5 -0
  54. package/dist/core/adapter.d.ts.map +1 -0
  55. package/dist/core/adapter.js +1 -0
  56. package/dist/core/adapters/attest.d.ts +5 -2
  57. package/dist/core/adapters/attest.d.ts.map +1 -1
  58. package/dist/core/adapters/attest.js +8 -3
  59. package/dist/core/adapters/profiles.d.ts +6 -6
  60. package/dist/core/adapters/profiles.d.ts.map +1 -1
  61. package/dist/core/adapters/profiles.js +4 -4
  62. package/dist/core/agents/canonical-emit.d.ts +17 -15
  63. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  64. package/dist/core/agents/canonical-emit.js +27 -25
  65. package/dist/core/agents/cli.js +212 -235
  66. package/dist/core/agents/codex-archive-v3.d.ts +14 -0
  67. package/dist/core/agents/codex-archive-v3.d.ts.map +1 -0
  68. package/dist/core/agents/codex-archive-v3.js +110 -0
  69. package/dist/core/agents/coord-client.d.ts +19 -70
  70. package/dist/core/agents/coord-client.d.ts.map +1 -1
  71. package/dist/core/agents/coord-client.js +65 -155
  72. package/dist/core/agents/finalization.d.ts +4 -1
  73. package/dist/core/agents/finalization.d.ts.map +1 -1
  74. package/dist/core/agents/finalization.js +89 -32
  75. package/dist/core/agents/index.d.ts +5 -5
  76. package/dist/core/agents/index.d.ts.map +1 -1
  77. package/dist/core/agents/index.js +5 -5
  78. package/dist/core/agents/live-authority-v3.d.ts +59 -0
  79. package/dist/core/agents/live-authority-v3.d.ts.map +1 -0
  80. package/dist/core/agents/live-authority-v3.js +233 -0
  81. package/dist/core/agents/live-lifecycle-v3.d.ts +20 -0
  82. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -0
  83. package/dist/core/agents/live-lifecycle-v3.js +91 -0
  84. package/dist/core/agents/live-observation-v3.d.ts +25 -0
  85. package/dist/core/agents/live-observation-v3.d.ts.map +1 -0
  86. package/dist/core/agents/live-observation-v3.js +157 -0
  87. package/dist/core/agents/render/prompt-context.d.ts +10 -4
  88. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  89. package/dist/core/agents/render/prompt-context.js +61 -55
  90. package/dist/core/agents/render/session-context.d.ts +1 -1
  91. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  92. package/dist/core/agents/render/session-context.js +15 -33
  93. package/dist/core/agents/rules/claim-conflict.d.ts +3 -3
  94. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  95. package/dist/core/agents/rules/claim-conflict.js +79 -127
  96. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  97. package/dist/core/agents/rules/commit-conflict.js +3 -18
  98. package/dist/core/agents/rules/stop-hook.d.ts +9 -50
  99. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  100. package/dist/core/agents/rules/stop-hook.js +193 -236
  101. package/dist/core/agents/session-events.d.ts +5 -7
  102. package/dist/core/agents/session-events.d.ts.map +1 -1
  103. package/dist/core/agents/session-events.js +127 -95
  104. package/dist/core/agents/session-finalization-state-v3.d.ts +30 -0
  105. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -0
  106. package/dist/core/agents/session-finalization-state-v3.js +40 -0
  107. package/dist/core/agents/session-finalizer-v3.d.ts +107 -0
  108. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -0
  109. package/dist/core/agents/session-finalizer-v3.js +734 -0
  110. package/dist/core/agents/session-name-display.d.ts +40 -0
  111. package/dist/core/agents/session-name-display.d.ts.map +1 -0
  112. package/dist/core/agents/session-name-display.js +119 -0
  113. package/dist/core/agents/state/activity-log.d.ts +3 -3
  114. package/dist/core/agents/state/activity-log.d.ts.map +1 -1
  115. package/dist/core/agents/state/activity-log.js +7 -16
  116. package/dist/core/agents/state/heartbeat-writer.d.ts +29 -46
  117. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  118. package/dist/core/agents/state/heartbeat-writer.js +59 -199
  119. package/dist/core/agents/state/live-coordination-view.d.ts +31 -0
  120. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -0
  121. package/dist/core/agents/state/live-coordination-view.js +209 -0
  122. package/dist/core/agents/state/session-state.d.ts +45 -0
  123. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  124. package/dist/core/agents/state/session-state.js +90 -0
  125. package/dist/core/agents/state/stale-sweep.d.ts +1 -1
  126. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  127. package/dist/core/agents/state/stale-sweep.js +26 -15
  128. package/dist/core/artifacts/index.d.ts.map +1 -1
  129. package/dist/core/artifacts/index.js +5 -7
  130. package/dist/core/config.d.ts +33 -2
  131. package/dist/core/config.d.ts.map +1 -1
  132. package/dist/core/config.js +98 -2
  133. package/dist/core/context/index.d.ts +1 -4
  134. package/dist/core/context/index.d.ts.map +1 -1
  135. package/dist/core/context/index.js +9 -9
  136. package/dist/core/events/span-parent.d.ts +15 -0
  137. package/dist/core/events/span-parent.d.ts.map +1 -0
  138. package/dist/core/events/span-parent.js +34 -0
  139. package/dist/core/events/v3/advance.d.ts +14 -0
  140. package/dist/core/events/v3/advance.d.ts.map +1 -0
  141. package/dist/core/events/v3/advance.js +134 -0
  142. package/dist/core/events/v3/authority-outbox.d.ts +78 -0
  143. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -0
  144. package/dist/core/events/v3/authority-outbox.js +399 -0
  145. package/dist/core/events/v3/base-builder.d.ts +20 -0
  146. package/dist/core/events/v3/base-builder.d.ts.map +1 -0
  147. package/dist/core/events/v3/base-builder.js +29 -0
  148. package/dist/core/events/v3/base-contract.d.ts +7576 -0
  149. package/dist/core/events/v3/base-contract.d.ts.map +1 -0
  150. package/dist/core/events/v3/base-contract.js +647 -0
  151. package/dist/core/events/v3/bootstrap.d.ts +34 -0
  152. package/dist/core/events/v3/bootstrap.d.ts.map +1 -0
  153. package/dist/core/events/v3/bootstrap.js +175 -0
  154. package/dist/core/events/v3/builder.d.ts +20 -0
  155. package/dist/core/events/v3/builder.d.ts.map +1 -0
  156. package/dist/core/events/v3/builder.js +28 -0
  157. package/dist/core/events/v3/canonical.d.ts +31 -0
  158. package/dist/core/events/v3/canonical.d.ts.map +1 -0
  159. package/dist/core/events/v3/canonical.js +100 -0
  160. package/dist/core/events/v3/capabilities.d.ts +31 -0
  161. package/dist/core/events/v3/capabilities.d.ts.map +1 -0
  162. package/dist/core/events/v3/capabilities.js +107 -0
  163. package/dist/core/events/v3/capability-drift.d.ts +22 -0
  164. package/dist/core/events/v3/capability-drift.d.ts.map +1 -0
  165. package/dist/core/events/v3/capability-drift.js +134 -0
  166. package/dist/core/events/v3/catalog.d.ts +38 -0
  167. package/dist/core/events/v3/catalog.d.ts.map +1 -0
  168. package/dist/core/events/v3/catalog.js +135 -0
  169. package/dist/core/events/v3/clock-order.d.ts +34 -0
  170. package/dist/core/events/v3/clock-order.d.ts.map +1 -0
  171. package/dist/core/events/v3/clock-order.js +35 -0
  172. package/dist/core/events/v3/contract.d.ts +171 -0
  173. package/dist/core/events/v3/contract.d.ts.map +1 -0
  174. package/dist/core/events/v3/contract.js +180 -0
  175. package/dist/core/events/v3/control.d.ts +111 -0
  176. package/dist/core/events/v3/control.d.ts.map +1 -0
  177. package/dist/core/events/v3/control.js +419 -0
  178. package/dist/core/events/v3/coordination-view.d.ts +69 -0
  179. package/dist/core/events/v3/coordination-view.d.ts.map +1 -0
  180. package/dist/core/events/v3/coordination-view.js +119 -0
  181. package/dist/core/events/v3/economics.d.ts +77 -0
  182. package/dist/core/events/v3/economics.d.ts.map +1 -0
  183. package/dist/core/events/v3/economics.js +274 -0
  184. package/dist/core/events/v3/finalization-view.d.ts +26 -0
  185. package/dist/core/events/v3/finalization-view.d.ts.map +1 -0
  186. package/dist/core/events/v3/finalization-view.js +37 -0
  187. package/dist/core/events/v3/fingerprint-keys.d.ts +25 -0
  188. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -0
  189. package/dist/core/events/v3/fingerprint-keys.js +217 -0
  190. package/dist/core/events/v3/generated.d.ts +4 -0
  191. package/dist/core/events/v3/generated.d.ts.map +1 -0
  192. package/dist/core/events/v3/generated.js +47 -0
  193. package/dist/core/events/v3/ids.d.ts +9 -0
  194. package/dist/core/events/v3/ids.d.ts.map +1 -0
  195. package/dist/core/events/v3/ids.js +25 -0
  196. package/dist/core/events/v3/index.d.ts +35 -0
  197. package/dist/core/events/v3/index.d.ts.map +1 -0
  198. package/dist/core/events/v3/index.js +33 -0
  199. package/dist/core/events/v3/latency.d.ts +63 -0
  200. package/dist/core/events/v3/latency.d.ts.map +1 -0
  201. package/dist/core/events/v3/latency.js +412 -0
  202. package/dist/core/events/v3/live-feed.d.ts +45 -0
  203. package/dist/core/events/v3/live-feed.d.ts.map +1 -0
  204. package/dist/core/events/v3/live-feed.js +246 -0
  205. package/dist/core/events/v3/live-observation.d.ts +35 -0
  206. package/dist/core/events/v3/live-observation.d.ts.map +1 -0
  207. package/dist/core/events/v3/live-observation.js +1 -0
  208. package/dist/core/events/v3/live-routing.d.ts +57 -0
  209. package/dist/core/events/v3/live-routing.d.ts.map +1 -0
  210. package/dist/core/events/v3/live-routing.js +107 -0
  211. package/dist/core/events/v3/producers/command-recorder.d.ts +67 -0
  212. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -0
  213. package/dist/core/events/v3/producers/command-recorder.js +477 -0
  214. package/dist/core/events/v3/producers/command.d.ts +50 -0
  215. package/dist/core/events/v3/producers/command.d.ts.map +1 -0
  216. package/dist/core/events/v3/producers/command.js +135 -0
  217. package/dist/core/events/v3/producers/coordination-recorder.d.ts +53 -0
  218. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -0
  219. package/dist/core/events/v3/producers/coordination-recorder.js +376 -0
  220. package/dist/core/events/v3/producers/coordination.d.ts +90 -0
  221. package/dist/core/events/v3/producers/coordination.d.ts.map +1 -0
  222. package/dist/core/events/v3/producers/coordination.js +229 -0
  223. package/dist/core/events/v3/producers/hook-base.d.ts +54 -0
  224. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -0
  225. package/dist/core/events/v3/producers/hook-base.js +424 -0
  226. package/dist/core/events/v3/producers/hook.d.ts +29 -0
  227. package/dist/core/events/v3/producers/hook.d.ts.map +1 -0
  228. package/dist/core/events/v3/producers/hook.js +71 -0
  229. package/dist/core/events/v3/producers/intake.d.ts +65 -0
  230. package/dist/core/events/v3/producers/intake.d.ts.map +1 -0
  231. package/dist/core/events/v3/producers/intake.js +244 -0
  232. package/dist/core/events/v3/producers/recorder.d.ts +240 -0
  233. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -0
  234. package/dist/core/events/v3/producers/recorder.js +2176 -0
  235. package/dist/core/events/v3/projection.d.ts +101 -0
  236. package/dist/core/events/v3/projection.d.ts.map +1 -0
  237. package/dist/core/events/v3/projection.js +479 -0
  238. package/dist/core/events/v3/reader.d.ts +75 -0
  239. package/dist/core/events/v3/reader.d.ts.map +1 -0
  240. package/dist/core/events/v3/reader.js +778 -0
  241. package/dist/core/events/v3/recovery-record.d.ts +47 -0
  242. package/dist/core/events/v3/recovery-record.d.ts.map +1 -0
  243. package/dist/core/events/v3/recovery-record.js +167 -0
  244. package/dist/core/events/v3/recovery.d.ts +22 -0
  245. package/dist/core/events/v3/recovery.d.ts.map +1 -0
  246. package/dist/core/events/v3/recovery.js +227 -0
  247. package/dist/core/events/v3/span-state.d.ts +52 -0
  248. package/dist/core/events/v3/span-state.d.ts.map +1 -0
  249. package/dist/core/events/v3/span-state.js +142 -0
  250. package/dist/core/events/v3/targets.d.ts +23 -0
  251. package/dist/core/events/v3/targets.d.ts.map +1 -0
  252. package/dist/core/events/v3/targets.js +134 -0
  253. package/dist/core/events/v3/turn-telemetry.d.ts +41 -0
  254. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -0
  255. package/dist/core/events/v3/turn-telemetry.js +168 -0
  256. package/dist/core/events/v3/validate.d.ts +9 -0
  257. package/dist/core/events/v3/validate.d.ts.map +1 -0
  258. package/dist/core/events/v3/validate.js +281 -0
  259. package/dist/core/events/v3/writer.d.ts +56 -0
  260. package/dist/core/events/v3/writer.d.ts.map +1 -0
  261. package/dist/core/events/v3/writer.js +335 -0
  262. package/dist/core/governor/index.d.ts +1 -1
  263. package/dist/core/governor/index.d.ts.map +1 -1
  264. package/dist/core/governor/index.js +1 -1
  265. package/dist/core/governor/plan-api.d.ts +2 -1
  266. package/dist/core/governor/plan-api.d.ts.map +1 -1
  267. package/dist/core/governor/plan-api.js +6 -3
  268. package/dist/core/governor/read.d.ts +1 -1
  269. package/dist/core/governor/read.d.ts.map +1 -1
  270. package/dist/core/governor/read.js +1 -1
  271. package/dist/core/governor/state.d.ts +9 -0
  272. package/dist/core/governor/state.d.ts.map +1 -1
  273. package/dist/core/governor/state.js +40 -3
  274. package/dist/core/guard/config.d.ts +5 -0
  275. package/dist/core/guard/config.d.ts.map +1 -0
  276. package/dist/core/guard/config.js +157 -0
  277. package/dist/core/guard/coordinator.d.ts +14 -0
  278. package/dist/core/guard/coordinator.d.ts.map +1 -0
  279. package/dist/core/guard/coordinator.js +173 -0
  280. package/dist/core/guard/evaluator.d.ts +4 -0
  281. package/dist/core/guard/evaluator.d.ts.map +1 -0
  282. package/dist/core/guard/evaluator.js +242 -0
  283. package/dist/core/guard/evidence-v3.d.ts +15 -0
  284. package/dist/core/guard/evidence-v3.d.ts.map +1 -0
  285. package/dist/core/guard/evidence-v3.js +171 -0
  286. package/dist/core/guard/index.d.ts +10 -0
  287. package/dist/core/guard/index.d.ts.map +1 -0
  288. package/dist/core/guard/index.js +6 -0
  289. package/dist/core/guard/live-source-v3.d.ts +40 -0
  290. package/dist/core/guard/live-source-v3.d.ts.map +1 -0
  291. package/dist/core/guard/live-source-v3.js +128 -0
  292. package/dist/core/guard/storage.d.ts +27 -0
  293. package/dist/core/guard/storage.d.ts.map +1 -0
  294. package/dist/core/guard/storage.js +140 -0
  295. package/dist/core/guard/types.d.ts +119 -0
  296. package/dist/core/guard/types.d.ts.map +1 -0
  297. package/dist/core/guard/types.js +1 -0
  298. package/dist/core/hooks/adapter/detect.d.ts +18 -1
  299. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  300. package/dist/core/hooks/adapter/detect.js +29 -0
  301. package/dist/core/hooks/adapter/events.d.ts +7 -4
  302. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  303. package/dist/core/hooks/adapter/events.js +13 -4
  304. package/dist/core/hooks/adapter/output.d.ts +2 -2
  305. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  306. package/dist/core/hooks/adapter/parse.d.ts +7 -4
  307. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  308. package/dist/core/hooks/adapter/parse.js +64 -24
  309. package/dist/core/hooks/adapter/wiring.d.ts +30 -4
  310. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  311. package/dist/core/hooks/adapter/wiring.js +89 -11
  312. package/dist/core/hooks/cli.d.ts +1 -1
  313. package/dist/core/hooks/cli.js +345 -400
  314. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  315. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  316. package/dist/core/hooks/codex-authorization.js +199 -0
  317. package/dist/core/hooks/effects/image-capture.d.ts +20 -34
  318. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  319. package/dist/core/hooks/effects/image-capture.js +212 -221
  320. package/dist/core/hooks/effects/index.d.ts +1 -9
  321. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  322. package/dist/core/hooks/effects/index.js +1 -24
  323. package/dist/core/hooks/events/input-hash.d.ts +7 -0
  324. package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
  325. package/dist/core/hooks/events/input-hash.js +43 -0
  326. package/dist/core/hooks/index.d.ts +4 -3
  327. package/dist/core/hooks/index.d.ts.map +1 -1
  328. package/dist/core/hooks/index.js +2 -3
  329. package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
  330. package/dist/core/hooks/resolve/coord-root.js +1 -1
  331. package/dist/core/hooks/resolve/owner.d.ts +16 -15
  332. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  333. package/dist/core/hooks/resolve/owner.js +51 -42
  334. package/dist/core/hooks/resolve/transcript.d.ts +33 -3
  335. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  336. package/dist/core/hooks/resolve/transcript.js +155 -14
  337. package/dist/core/hooks/session-name-presence.d.ts +9 -10
  338. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  339. package/dist/core/hooks/session-name-presence.js +15 -15
  340. package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
  341. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
  342. package/dist/core/journal/index.d.ts +2 -2
  343. package/dist/core/journal/index.d.ts.map +1 -1
  344. package/dist/core/journal/index.js +9 -22
  345. package/dist/core/presence/blob.d.ts +8 -3
  346. package/dist/core/presence/blob.d.ts.map +1 -1
  347. package/dist/core/presence/blob.js +54 -41
  348. package/dist/core/presence/codec-digest.d.ts +29 -0
  349. package/dist/core/presence/codec-digest.d.ts.map +1 -0
  350. package/dist/core/presence/codec-digest.js +170 -0
  351. package/dist/core/presence/index.js +15 -1
  352. package/dist/core/presence/relay-client.d.ts +1 -1
  353. package/dist/core/presence/relay-client.d.ts.map +1 -1
  354. package/dist/core/presence/relay-client.js +5 -5
  355. package/dist/core/work/index.d.ts +1 -1
  356. package/dist/core/work/index.d.ts.map +1 -1
  357. package/dist/core/work/index.js +1 -1
  358. package/dist/core/work/read.d.ts +1 -1
  359. package/dist/core/work/read.d.ts.map +1 -1
  360. package/dist/core/work/read.js +1 -1
  361. package/dist/core/work/runner.d.ts.map +1 -1
  362. package/dist/core/work/runner.js +6 -0
  363. package/dist/core/work/state.d.ts +9 -0
  364. package/dist/core/work/state.d.ts.map +1 -1
  365. package/dist/core/work/state.js +17 -3
  366. package/dist/core/workflow/engine.js +24 -25
  367. package/dist/core/workflow/live-session-v3.d.ts +19 -0
  368. package/dist/core/workflow/live-session-v3.d.ts.map +1 -0
  369. package/dist/core/workflow/live-session-v3.js +86 -0
  370. package/dist/core/workflow/scope-id.d.ts +3 -0
  371. package/dist/core/workflow/scope-id.d.ts.map +1 -0
  372. package/dist/core/workflow/scope-id.js +5 -0
  373. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  374. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  375. package/dist/core/workflow/workspaces/leases.js +53 -19
  376. package/dist/index.d.ts +1 -1
  377. package/dist/index.js +1 -1
  378. package/dist/lib/browser/client.d.ts +41 -0
  379. package/dist/lib/browser/client.d.ts.map +1 -1
  380. package/dist/lib/browser/client.js +203 -0
  381. package/dist/lib/browser/critique.d.ts +28 -2
  382. package/dist/lib/browser/critique.d.ts.map +1 -1
  383. package/dist/lib/browser/critique.js +74 -18
  384. package/dist/lib/browser/geometry.d.ts +1 -1
  385. package/dist/lib/browser/geometry.d.ts.map +1 -1
  386. package/dist/lib/browser/geometry.js +130 -6
  387. package/dist/lib/browser/index.d.ts +3 -0
  388. package/dist/lib/browser/index.d.ts.map +1 -1
  389. package/dist/lib/browser/index.js +3 -0
  390. package/dist/lib/browser/qa-plan.d.ts +174 -0
  391. package/dist/lib/browser/qa-plan.d.ts.map +1 -0
  392. package/dist/lib/browser/qa-plan.js +321 -0
  393. package/dist/lib/browser/qa-reuse.d.ts +99 -0
  394. package/dist/lib/browser/qa-reuse.d.ts.map +1 -0
  395. package/dist/lib/browser/qa-reuse.js +222 -0
  396. package/dist/lib/browser/qa-snapshot.d.ts +64 -0
  397. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -0
  398. package/dist/lib/browser/qa-snapshot.js +171 -0
  399. package/dist/lib/browser/runts.d.ts.map +1 -1
  400. package/dist/lib/browser/runts.js +22 -1
  401. package/dist/lib/browser/tiling.d.ts +71 -0
  402. package/dist/lib/browser/tiling.d.ts.map +1 -0
  403. package/dist/lib/browser/tiling.js +118 -0
  404. package/dist/lib/context/index.d.ts +0 -3
  405. package/dist/lib/context/index.d.ts.map +1 -1
  406. package/dist/lib/context/index.js +20 -42
  407. package/dist/lib/council/index.d.ts +2 -2
  408. package/dist/lib/council/index.d.ts.map +1 -1
  409. package/dist/lib/council/index.js +9 -20
  410. package/dist/lib/docs-frontmatter.d.ts +1 -6
  411. package/dist/lib/docs-frontmatter.d.ts.map +1 -1
  412. package/dist/lib/docs-frontmatter.js +12 -78
  413. package/dist/lib/docs-index.d.ts +9 -0
  414. package/dist/lib/docs-index.d.ts.map +1 -1
  415. package/dist/lib/docs-index.js +32 -3
  416. package/dist/lib/docs-lint.d.ts.map +1 -1
  417. package/dist/lib/docs-lint.js +14 -30
  418. package/dist/lib/docs-metadata-audit.d.ts +19 -0
  419. package/dist/lib/docs-metadata-audit.d.ts.map +1 -0
  420. package/dist/lib/docs-metadata-audit.js +132 -0
  421. package/dist/lib/docs-metadata-managed.d.ts +23 -0
  422. package/dist/lib/docs-metadata-managed.d.ts.map +1 -0
  423. package/dist/lib/docs-metadata-managed.js +78 -0
  424. package/dist/lib/docs-metadata-sync.d.ts +20 -0
  425. package/dist/lib/docs-metadata-sync.d.ts.map +1 -0
  426. package/dist/lib/docs-metadata-sync.js +222 -0
  427. package/dist/lib/docs-metadata-v2-schema.d.ts +273 -0
  428. package/dist/lib/docs-metadata-v2-schema.d.ts.map +1 -0
  429. package/dist/lib/docs-metadata-v2-schema.js +134 -0
  430. package/dist/lib/docs-metadata-v2.d.ts +21 -0
  431. package/dist/lib/docs-metadata-v2.d.ts.map +1 -0
  432. package/dist/lib/docs-metadata-v2.js +424 -0
  433. package/dist/lib/docs-new.d.ts +11 -0
  434. package/dist/lib/docs-new.d.ts.map +1 -0
  435. package/dist/lib/docs-new.js +52 -0
  436. package/dist/lib/docs-sweep.d.ts.map +1 -1
  437. package/dist/lib/docs-sweep.js +45 -29
  438. package/dist/lib/headless/index.d.ts +98 -0
  439. package/dist/lib/headless/index.d.ts.map +1 -0
  440. package/dist/lib/headless/index.js +211 -0
  441. package/dist/lib/identities/assume.d.ts +4 -4
  442. package/dist/lib/identities/assume.d.ts.map +1 -1
  443. package/dist/lib/identities/assume.js +52 -68
  444. package/dist/lib/instructions/apply.d.ts +5 -4
  445. package/dist/lib/instructions/apply.d.ts.map +1 -1
  446. package/dist/lib/instructions/apply.js +32 -20
  447. package/dist/lib/instructions/bundle.d.ts +33 -0
  448. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  449. package/dist/lib/instructions/bundle.js +149 -0
  450. package/dist/lib/instructions/splice.d.ts +0 -1
  451. package/dist/lib/instructions/splice.d.ts.map +1 -1
  452. package/dist/lib/instructions/splice.js +22 -6
  453. package/dist/lib/instructions/templates.d.ts +7 -5
  454. package/dist/lib/instructions/templates.d.ts.map +1 -1
  455. package/dist/lib/instructions/templates.js +79 -9
  456. package/package.json +27 -6
  457. package/schemas/config.schema.json +120 -2
  458. package/schemas/docs-metadata-v2.schema.json +441 -0
  459. package/schemas/event-v3.schema.json +24472 -0
  460. package/src/commander.ts +7 -1
  461. package/src/commands/adapter.ts +2 -2
  462. package/src/commands/agents.ts +1737 -570
  463. package/src/commands/artifacts.ts +7 -6
  464. package/src/commands/backup.ts +29 -21
  465. package/src/commands/browse.ts +349 -8
  466. package/src/commands/checkpoint.ts +4 -3
  467. package/src/commands/claude-desktop.ts +7 -5
  468. package/src/commands/context.ts +1 -6
  469. package/src/commands/decision.ts +27 -44
  470. package/src/commands/docs.ts +100 -33
  471. package/src/commands/doctor.ts +73 -8
  472. package/src/commands/events.ts +256 -0
  473. package/src/commands/governor.ts +18 -4
  474. package/src/commands/init.ts +264 -57
  475. package/src/commands/instructions.ts +129 -0
  476. package/src/commands/journal.ts +4 -13
  477. package/src/commands/ledger-v3.ts +111 -0
  478. package/src/commands/presence.ts +18 -11
  479. package/src/commands/sync.ts +1 -1
  480. package/src/commands/web.ts +67 -8
  481. package/src/commands/work.ts +28 -5
  482. package/src/core/adapter.ts +5 -0
  483. package/src/core/adapters/attest.ts +8 -3
  484. package/src/core/adapters/profiles.ts +4 -6
  485. package/src/core/agents/canonical-emit.ts +39 -28
  486. package/src/core/agents/cli.ts +242 -263
  487. package/src/core/agents/codex-archive-v3.ts +138 -0
  488. package/src/core/agents/coord-client.ts +75 -177
  489. package/src/core/agents/finalization.ts +120 -38
  490. package/src/core/agents/index.ts +5 -5
  491. package/src/core/agents/live-authority-v3.ts +375 -0
  492. package/src/core/agents/live-lifecycle-v3.ts +140 -0
  493. package/src/core/agents/live-observation-v3.ts +216 -0
  494. package/src/core/agents/render/prompt-context.ts +89 -55
  495. package/src/core/agents/render/session-context.ts +23 -32
  496. package/src/core/agents/rules/claim-conflict.ts +85 -126
  497. package/src/core/agents/rules/commit-conflict.ts +3 -14
  498. package/src/core/agents/rules/stop-hook.ts +239 -293
  499. package/src/core/agents/session-events.ts +137 -104
  500. package/src/core/agents/session-finalization-state-v3.ts +90 -0
  501. package/src/core/agents/session-finalizer-v3.ts +953 -0
  502. package/src/core/agents/session-name-display.ts +149 -0
  503. package/src/core/agents/state/activity-log.ts +6 -14
  504. package/src/core/agents/state/heartbeat-writer.ts +98 -242
  505. package/src/core/agents/state/live-coordination-view.ts +290 -0
  506. package/src/core/agents/state/session-state.ts +143 -0
  507. package/src/core/agents/state/stale-sweep.ts +40 -17
  508. package/src/core/artifacts/index.ts +4 -5
  509. package/src/core/config.ts +148 -3
  510. package/src/core/context/index.ts +9 -17
  511. package/src/core/events/span-parent.ts +46 -0
  512. package/src/core/events/v3/advance.ts +168 -0
  513. package/src/core/events/v3/authority-outbox.ts +548 -0
  514. package/src/core/events/v3/base-builder.ts +60 -0
  515. package/src/core/events/v3/base-contract.ts +882 -0
  516. package/src/core/events/v3/bootstrap.ts +226 -0
  517. package/src/core/events/v3/builder.ts +59 -0
  518. package/src/core/events/v3/canonical.ts +153 -0
  519. package/src/core/events/v3/capabilities.ts +171 -0
  520. package/src/core/events/v3/capability-drift.ts +177 -0
  521. package/src/core/events/v3/catalog.ts +202 -0
  522. package/src/core/events/v3/clock-order.ts +41 -0
  523. package/src/core/events/v3/contract.ts +365 -0
  524. package/src/core/events/v3/control.ts +576 -0
  525. package/src/core/events/v3/coordination-view.ts +193 -0
  526. package/src/core/events/v3/economics.ts +384 -0
  527. package/src/core/events/v3/finalization-view.ts +67 -0
  528. package/src/core/events/v3/fingerprint-keys.ts +260 -0
  529. package/src/core/events/v3/generated.ts +48 -0
  530. package/src/core/events/v3/ids.ts +33 -0
  531. package/src/core/events/v3/index.ts +322 -0
  532. package/src/core/events/v3/latency.ts +551 -0
  533. package/src/core/events/v3/live-feed.ts +319 -0
  534. package/src/core/events/v3/live-observation.ts +39 -0
  535. package/src/core/events/v3/live-routing.ts +135 -0
  536. package/src/core/events/v3/producers/command-recorder.ts +631 -0
  537. package/src/core/events/v3/producers/command.ts +201 -0
  538. package/src/core/events/v3/producers/coordination-recorder.ts +538 -0
  539. package/src/core/events/v3/producers/coordination.ts +355 -0
  540. package/src/core/events/v3/producers/hook-base.ts +578 -0
  541. package/src/core/events/v3/producers/hook.ts +119 -0
  542. package/src/core/events/v3/producers/intake.ts +319 -0
  543. package/src/core/events/v3/producers/recorder.ts +2805 -0
  544. package/src/core/events/v3/projection.ts +653 -0
  545. package/src/core/events/v3/reader.ts +1127 -0
  546. package/src/core/events/v3/recovery-record.ts +244 -0
  547. package/src/core/events/v3/recovery.ts +271 -0
  548. package/src/core/events/v3/span-state.ts +187 -0
  549. package/src/core/events/v3/targets.ts +182 -0
  550. package/src/core/events/v3/turn-telemetry.ts +259 -0
  551. package/src/core/events/v3/validate.ts +361 -0
  552. package/src/core/events/v3/writer.ts +419 -0
  553. package/src/core/governor/index.ts +3 -0
  554. package/src/core/governor/plan-api.ts +10 -2
  555. package/src/core/governor/read.ts +3 -0
  556. package/src/core/governor/state.ts +52 -5
  557. package/src/core/guard/config.ts +206 -0
  558. package/src/core/guard/coordinator.ts +227 -0
  559. package/src/core/guard/evaluator.ts +343 -0
  560. package/src/core/guard/evidence-v3.ts +227 -0
  561. package/src/core/guard/index.ts +35 -0
  562. package/src/core/guard/live-source-v3.ts +204 -0
  563. package/src/core/guard/storage.ts +188 -0
  564. package/src/core/guard/types.ts +152 -0
  565. package/src/core/hooks/adapter/detect.ts +28 -1
  566. package/src/core/hooks/adapter/events.ts +15 -6
  567. package/src/core/hooks/adapter/output.ts +7 -2
  568. package/src/core/hooks/adapter/parse.ts +81 -36
  569. package/src/core/hooks/adapter/wiring.ts +119 -13
  570. package/src/core/hooks/cli.ts +398 -422
  571. package/src/core/hooks/codex-authorization.ts +253 -0
  572. package/src/core/hooks/effects/image-capture.ts +228 -238
  573. package/src/core/hooks/effects/index.ts +1 -36
  574. package/src/core/hooks/events/input-hash.ts +47 -0
  575. package/src/core/hooks/index.ts +4 -11
  576. package/src/core/hooks/resolve/coord-root.ts +1 -1
  577. package/src/core/hooks/resolve/owner.ts +52 -45
  578. package/src/core/hooks/resolve/transcript.ts +165 -14
  579. package/src/core/hooks/session-name-presence.ts +14 -15
  580. package/src/core/hooks/unsafe-cross-shell.ts +1 -1
  581. package/src/core/journal/index.ts +15 -22
  582. package/src/core/presence/blob.ts +63 -38
  583. package/src/core/presence/codec-digest.ts +215 -0
  584. package/src/core/presence/index.ts +18 -1
  585. package/src/core/presence/relay-client.ts +5 -5
  586. package/src/core/work/index.ts +3 -0
  587. package/src/core/work/read.ts +3 -0
  588. package/src/core/work/runner.ts +6 -0
  589. package/src/core/work/state.ts +27 -3
  590. package/src/core/workflow/engine.ts +24 -24
  591. package/src/core/workflow/live-session-v3.ts +126 -0
  592. package/src/core/workflow/scope-id.ts +6 -0
  593. package/src/core/workflow/workspaces/leases.ts +56 -19
  594. package/src/index.ts +1 -1
  595. package/src/lib/browser/client.ts +230 -0
  596. package/src/lib/browser/critique.ts +104 -27
  597. package/src/lib/browser/geometry.ts +143 -14
  598. package/src/lib/browser/index.ts +34 -0
  599. package/src/lib/browser/qa-plan.ts +501 -0
  600. package/src/lib/browser/qa-reuse.ts +306 -0
  601. package/src/lib/browser/qa-snapshot.ts +223 -0
  602. package/src/lib/browser/runts.ts +27 -2
  603. package/src/lib/browser/tiling.ts +164 -0
  604. package/src/lib/context/index.ts +22 -46
  605. package/src/lib/council/index.ts +9 -21
  606. package/src/lib/docs-frontmatter.ts +8 -79
  607. package/src/lib/docs-index.ts +33 -3
  608. package/src/lib/docs-lint.ts +15 -29
  609. package/src/lib/docs-metadata-audit.ts +166 -0
  610. package/src/lib/docs-metadata-managed.ts +83 -0
  611. package/src/lib/docs-metadata-sync.ts +262 -0
  612. package/src/lib/docs-metadata-v2-schema.ts +138 -0
  613. package/src/lib/docs-metadata-v2.ts +646 -0
  614. package/src/lib/docs-new.ts +63 -0
  615. package/src/lib/docs-sweep.ts +42 -28
  616. package/src/lib/headless/index.ts +287 -0
  617. package/src/lib/identities/assume.ts +67 -86
  618. package/src/lib/instructions/apply.ts +33 -20
  619. package/src/lib/instructions/bundle.ts +199 -0
  620. package/src/lib/instructions/splice.ts +21 -7
  621. package/src/lib/instructions/templates.ts +85 -13
  622. package/dist/core/agents/cli-emit.d.ts +0 -27
  623. package/dist/core/agents/cli-emit.d.ts.map +0 -1
  624. package/dist/core/agents/cli-emit.js +0 -57
  625. package/dist/core/agents/codex-replay.d.ts +0 -29
  626. package/dist/core/agents/codex-replay.d.ts.map +0 -1
  627. package/dist/core/agents/codex-replay.js +0 -138
  628. package/dist/core/agents/events/consume.d.ts +0 -82
  629. package/dist/core/agents/events/consume.d.ts.map +0 -1
  630. package/dist/core/agents/events/consume.js +0 -195
  631. package/dist/core/agents/events/emit.d.ts +0 -42
  632. package/dist/core/agents/events/emit.d.ts.map +0 -1
  633. package/dist/core/agents/events/emit.js +0 -75
  634. package/dist/core/agents/events/ulid.d.ts +0 -11
  635. package/dist/core/agents/events/ulid.d.ts.map +0 -1
  636. package/dist/core/agents/events/ulid.js +0 -47
  637. package/dist/core/agents/state/heartbeat-projector.d.ts +0 -65
  638. package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
  639. package/dist/core/agents/state/heartbeat-projector.js +0 -527
  640. package/dist/core/hooks/events/emit.d.ts +0 -31
  641. package/dist/core/hooks/events/emit.d.ts.map +0 -1
  642. package/dist/core/hooks/events/emit.js +0 -93
  643. package/dist/core/hooks/events/rotate.d.ts +0 -43
  644. package/dist/core/hooks/events/rotate.d.ts.map +0 -1
  645. package/dist/core/hooks/events/rotate.js +0 -142
  646. package/dist/core/hooks/events/schema.d.ts +0 -312
  647. package/dist/core/hooks/events/schema.d.ts.map +0 -1
  648. package/dist/core/hooks/events/schema.js +0 -12
  649. package/dist/core/hooks/events/ulid.d.ts +0 -10
  650. package/dist/core/hooks/events/ulid.d.ts.map +0 -1
  651. package/dist/core/hooks/events/ulid.js +0 -47
  652. package/dist/lib/docs-frontmatter-migrate.d.ts +0 -33
  653. package/dist/lib/docs-frontmatter-migrate.d.ts.map +0 -1
  654. package/dist/lib/docs-frontmatter-migrate.js +0 -364
  655. package/src/core/agents/cli-emit.ts +0 -64
  656. package/src/core/agents/codex-replay.ts +0 -163
  657. package/src/core/agents/events/consume.ts +0 -254
  658. package/src/core/agents/events/emit.ts +0 -115
  659. package/src/core/agents/events/ulid.ts +0 -51
  660. package/src/core/agents/state/heartbeat-projector.ts +0 -582
  661. package/src/core/hooks/events/emit.ts +0 -125
  662. package/src/core/hooks/events/rotate.ts +0 -151
  663. package/src/core/hooks/events/schema.ts +0 -538
  664. package/src/core/hooks/events/ulid.ts +0 -51
  665. package/src/lib/docs-frontmatter-migrate.ts +0 -427
@@ -0,0 +1,321 @@
1
+ // Diff-aware QA planner: classify a page change against a baseline signature
2
+ // and emit a machine-readable review manifest BEFORE any expensive work runs.
3
+ //
4
+ // The motivating failure is spend without proportionality: a one-word copy
5
+ // edit and a theme rewrite both trigger the same full-page, every-context
6
+ // vision review. This module makes review proportional to the change:
7
+ // - text/data-only edits produce zero vision calls;
8
+ // - a local component change reviews only that component's stable root;
9
+ // - global CSS, unprovable blast radius, or a missing baseline widen to
10
+ // full coverage — ambiguity always widens, never silently downgrades.
11
+ //
12
+ // Everything here is pure data-in/data-out: signatures are captured elsewhere
13
+ // (the browser client), baselines are persisted elsewhere (qa-snapshot.ts),
14
+ // and execution happens elsewhere (the critique engine). Keeping the planner
15
+ // pure means the classification can be reviewed, logged, and overridden
16
+ // before a single model call is made.
17
+ //
18
+ // One deliberate portability boundary: a stylesheet change classifies as
19
+ // `large-structural` here because CSS blast radius is not provable from
20
+ // digests alone. A host that can map component-scoped stylesheets to
21
+ // selectors can downgrade after the fact; the portable layer never does.
22
+ /** FNV-1a 32-bit over a string, hex-encoded, with a length suffix so the
23
+ * (tiny) collision surface also has to match on length. Used for text and
24
+ * stylesheet digests — a comparison fingerprint, not a security hash. */
25
+ export function fnv1a32(text) {
26
+ let hash = 0x811c9dc5;
27
+ for (let i = 0; i < text.length; i++) {
28
+ hash ^= text.charCodeAt(i);
29
+ hash = Math.imul(hash, 0x01000193);
30
+ }
31
+ return `${(hash >>> 0).toString(16).padStart(8, "0")}-${text.length}`;
32
+ }
33
+ const UNAVAILABLE = "unavailable";
34
+ function diffStylesheets(baseline, current) {
35
+ const base = new Map(baseline.map((s) => [s.key, s.digest]));
36
+ const cur = new Map(current.map((s) => [s.key, s.digest]));
37
+ const unavailable = [...baseline, ...current].some((s) => s.digest === UNAVAILABLE);
38
+ const changed = [];
39
+ for (const [key, digest] of cur) {
40
+ const prior = base.get(key);
41
+ if (prior === undefined || prior !== digest)
42
+ changed.push(key);
43
+ }
44
+ for (const key of base.keys()) {
45
+ if (!cur.has(key))
46
+ changed.push(key);
47
+ }
48
+ return { changed: changed.sort(), unavailable };
49
+ }
50
+ /**
51
+ * Classify current against baseline. Pure: no browser, no filesystem. The
52
+ * bias is stated in the plan and enforced here — ambiguity widens (to
53
+ * `unknown`/`large-structural`), never narrows. A `null` baseline is the
54
+ * "no baseline available" case and is always `unknown`.
55
+ */
56
+ export function classifySignatures(baseline, current, opts = {}) {
57
+ const maxScopes = opts.maxScopes ?? 4;
58
+ const none = {
59
+ text_changed_paths: [],
60
+ structural_changed_paths: [],
61
+ stylesheets_changed: [],
62
+ scopes: [],
63
+ identical: false,
64
+ };
65
+ if (!baseline) {
66
+ return {
67
+ ...none,
68
+ change_class: "unknown",
69
+ reasons: ["no baseline available — cannot prove any change class"],
70
+ };
71
+ }
72
+ if (baseline.truncated || current.truncated) {
73
+ return {
74
+ ...none,
75
+ change_class: "unknown",
76
+ reasons: [
77
+ "signature capture was truncated at its node cap — absence of change cannot be proven",
78
+ ],
79
+ };
80
+ }
81
+ const sheets = diffStylesheets(baseline.stylesheets, current.stylesheets);
82
+ if (sheets.unavailable) {
83
+ return {
84
+ ...none,
85
+ change_class: "unknown",
86
+ stylesheets_changed: sheets.changed,
87
+ reasons: [
88
+ "stylesheet digest unavailable (cross-origin without CORS) — style stability cannot be proven, DOM equality alone never proves text-only",
89
+ ],
90
+ };
91
+ }
92
+ const baseNodes = new Map(baseline.nodes.map((n) => [n.path, n]));
93
+ const curNodes = new Map(current.nodes.map((n) => [n.path, n]));
94
+ const unavailableVisual = [...baseline.nodes, ...current.nodes].find((node) => node.visual === UNAVAILABLE);
95
+ if (unavailableVisual) {
96
+ return {
97
+ ...none,
98
+ change_class: "unknown",
99
+ reasons: [
100
+ `visual digest unavailable at ${unavailableVisual.path} — text-only cannot be proven`,
101
+ ],
102
+ };
103
+ }
104
+ const textChanged = [];
105
+ const structuralChanged = new Map();
106
+ for (const [path, node] of curNodes) {
107
+ const prior = baseNodes.get(path);
108
+ if (!prior) {
109
+ structuralChanged.set(path, node); // added
110
+ }
111
+ else if (prior.tag !== node.tag ||
112
+ prior.attrs !== node.attrs ||
113
+ (prior.visual ?? "") !== (node.visual ?? "")) {
114
+ structuralChanged.set(path, node);
115
+ }
116
+ else if ((prior.text ?? "") !== (node.text ?? "")) {
117
+ textChanged.push(path);
118
+ }
119
+ }
120
+ for (const [path, node] of baseNodes) {
121
+ if (!curNodes.has(path) && !structuralChanged.has(path)) {
122
+ structuralChanged.set(path, node); // removed — anchor comes from baseline side
123
+ }
124
+ }
125
+ if (sheets.changed.length > 0) {
126
+ return {
127
+ ...none,
128
+ change_class: "large-structural",
129
+ text_changed_paths: textChanged.sort(),
130
+ structural_changed_paths: [...structuralChanged.keys()].sort(),
131
+ stylesheets_changed: sheets.changed,
132
+ reasons: [
133
+ `stylesheet changed (${sheets.changed.join(", ")}) — CSS blast radius is not provable portably, widening to full coverage`,
134
+ ],
135
+ };
136
+ }
137
+ if (structuralChanged.size === 0) {
138
+ if (textChanged.length === 0) {
139
+ return {
140
+ ...none,
141
+ change_class: "text-data-only",
142
+ identical: true,
143
+ reasons: ["no differences detected between baseline and current signatures"],
144
+ };
145
+ }
146
+ return {
147
+ ...none,
148
+ change_class: "text-data-only",
149
+ text_changed_paths: textChanged.sort(),
150
+ reasons: [
151
+ `only direct text content changed (${textChanged.length} node${textChanged.length === 1 ? "" : "s"}); element structure, attributes, and stylesheet digests are unchanged`,
152
+ ],
153
+ };
154
+ }
155
+ // Structural changes: lift every changed node to its nearest strict-ancestor
156
+ // stable anchor. Any node without one is an unprovable boundary — widen.
157
+ const anchorless = [];
158
+ const scopeNodes = new Map();
159
+ for (const [path, node] of structuralChanged) {
160
+ if (!node.anchor) {
161
+ anchorless.push(path);
162
+ continue;
163
+ }
164
+ const existing = scopeNodes.get(node.anchor.selector);
165
+ if (existing)
166
+ existing.count++;
167
+ else
168
+ scopeNodes.set(node.anchor.selector, { path: node.anchor.path, count: 1 });
169
+ }
170
+ if (anchorless.length > 0) {
171
+ return {
172
+ ...none,
173
+ change_class: "large-structural",
174
+ text_changed_paths: textChanged.sort(),
175
+ structural_changed_paths: [...structuralChanged.keys()].sort(),
176
+ reasons: [
177
+ `${anchorless.length} changed node${anchorless.length === 1 ? "" : "s"} without a stable ancestor (first: ${anchorless.sort()[0]}) — visual impact cannot be proven to stay inside stable selectors`,
178
+ ],
179
+ };
180
+ }
181
+ // Dedupe nested anchors to the outermost root: reviewing a container once
182
+ // beats one model call per anchored descendant.
183
+ const entries = [...scopeNodes.entries()].sort((a, b) => a[1].path.length - b[1].path.length);
184
+ const roots = [];
185
+ for (const entry of entries) {
186
+ const container = roots.find(([, root]) => entry[1].path.startsWith(`${root.path}>`));
187
+ if (container)
188
+ container[1].count += entry[1].count;
189
+ else
190
+ roots.push(entry);
191
+ }
192
+ if (roots.length > maxScopes) {
193
+ return {
194
+ ...none,
195
+ change_class: "large-structural",
196
+ text_changed_paths: textChanged.sort(),
197
+ structural_changed_paths: [...structuralChanged.keys()].sort(),
198
+ reasons: [
199
+ `changes span ${roots.length} independent scope roots (limit ${maxScopes}) — a multi-region change is a page-level change`,
200
+ ],
201
+ };
202
+ }
203
+ return {
204
+ change_class: "local-visual",
205
+ identical: false,
206
+ text_changed_paths: textChanged.sort(),
207
+ structural_changed_paths: [...structuralChanged.keys()].sort(),
208
+ stylesheets_changed: [],
209
+ scopes: roots
210
+ .sort((a, b) => a[0].localeCompare(b[0]))
211
+ .map(([selector, root]) => ({
212
+ selector,
213
+ reason: `nearest stable ancestor of ${root.count} changed node${root.count === 1 ? "" : "s"}`,
214
+ matches: 1,
215
+ })),
216
+ reasons: [
217
+ `structural changes contained by ${roots.length} stable scope root${roots.length === 1 ? "" : "s"}`,
218
+ ],
219
+ };
220
+ }
221
+ const DEFAULT_DETERMINISTIC = ["console", "overflow", "truncation", "placeholder"];
222
+ /**
223
+ * Turn a classification into an executable review manifest. Pure: rendering,
224
+ * baseline acquisition, and critique execution stay in the caller. The
225
+ * separation is the point — the manifest can be printed by a dry run,
226
+ * reviewed, and overridden before expensive work starts.
227
+ */
228
+ export function buildQaManifest(classification, opts) {
229
+ const viewports = opts.viewports ?? ["desktop", "mobile"];
230
+ const fullPageTiles = opts.estimatedFullPageTiles ?? 24;
231
+ const states = opts.states ?? [];
232
+ const scopes = opts.explicitScopes?.length ? opts.explicitScopes : classification.scopes;
233
+ let changeClass = classification.change_class;
234
+ const reasons = [...classification.reasons];
235
+ if (opts.explicitScopes?.length && changeClass === "unknown") {
236
+ // Explicit scope input is resolution-order rung 1: the producing task
237
+ // knows its own blast radius better than a missing baseline does.
238
+ changeClass = "local-visual";
239
+ reasons.push("explicit scope selectors supplied — overriding unknown classification");
240
+ }
241
+ if (opts.explicitScopes?.length &&
242
+ changeClass === "large-structural" &&
243
+ classification.stylesheets_changed.length > 0 &&
244
+ classification.structural_changed_paths.length === 0) {
245
+ changeClass = "local-visual";
246
+ reasons.push("explicit scope selectors supplied for a stylesheet-only change — host proved the component boundary");
247
+ }
248
+ if (states.length > 0 && (changeClass === "text-data-only" || changeClass === "local-visual")) {
249
+ changeClass = "interaction-state";
250
+ reasons.push(`explicit interaction states requested: ${states.join(", ")}`);
251
+ }
252
+ let incomplete;
253
+ if (classification.change_class === "unknown" && changeClass === "unknown") {
254
+ if (opts.stopOnUnknown) {
255
+ incomplete = { reason: classification.reasons[0] ?? "classification unknown" };
256
+ }
257
+ else {
258
+ reasons.push("unknown class widened to large-structural coverage");
259
+ }
260
+ }
261
+ if ((changeClass === "local-visual" || changeClass === "interaction-state") &&
262
+ (scopes.length === 0 || scopes.some((scope) => scope.matches < 1))) {
263
+ const missing = scopes.find((scope) => scope.matches < 1)?.selector;
264
+ incomplete = {
265
+ reason: missing
266
+ ? `scope selector matched no elements: ${missing}`
267
+ : `${changeClass} requires at least one stable scope selector`,
268
+ };
269
+ }
270
+ const matrix = viewports.flatMap((viewport) => ["light", "dark"].map((theme) => ({ viewport, theme, state: "default" })));
271
+ let contexts;
272
+ let visual;
273
+ switch (changeClass) {
274
+ case "text-data-only":
275
+ contexts = [];
276
+ visual = "none";
277
+ break;
278
+ case "local-visual":
279
+ contexts = viewports.map((viewport) => ({ viewport, theme: "light", state: "default" }));
280
+ visual = "scoped";
281
+ break;
282
+ case "interaction-state":
283
+ contexts = [
284
+ { viewport: viewports[0] ?? "desktop", theme: "light", state: "default" },
285
+ ...states.map((state) => ({
286
+ viewport: viewports[0] ?? "desktop",
287
+ theme: "light",
288
+ state,
289
+ })),
290
+ ];
291
+ visual = "scoped";
292
+ break;
293
+ default:
294
+ contexts = incomplete ? [] : matrix;
295
+ visual = "full-page";
296
+ break;
297
+ }
298
+ if (incomplete)
299
+ contexts = [];
300
+ const tilesPerContext = visual === "none" ? 0 : visual === "scoped" ? Math.max(1, scopes.length) : fullPageTiles;
301
+ const tilesCeiling = tilesPerContext * contexts.length;
302
+ return {
303
+ schema_version: 1,
304
+ change_class: changeClass,
305
+ classification_reasons: reasons,
306
+ ...(opts.sourceRevision ? { source_revision: opts.sourceRevision } : {}),
307
+ ...(opts.baselineRevision ? { baseline_revision: opts.baselineRevision } : {}),
308
+ baseline_source: opts.baselineSource,
309
+ scopes: visual === "none" ? [] : scopes,
310
+ contexts,
311
+ checks: {
312
+ deterministic: opts.deterministicChecks ?? DEFAULT_DETERMINISTIC,
313
+ interaction: opts.outcomeAssertions ?? [],
314
+ visual,
315
+ },
316
+ concurrency: opts.concurrency ?? { headless: 4, metered: 2 },
317
+ reuse: opts.reuse ?? { mode: "none", cache: false },
318
+ predicted: { tiles_ceiling: tilesCeiling, model_calls_ceiling: tilesCeiling },
319
+ ...(incomplete ? { incomplete } : {}),
320
+ };
321
+ }
@@ -0,0 +1,99 @@
1
+ import { PNG } from "pngjs";
2
+ import type { CritiqueFinding, CritiqueTile } from "./critique.js";
3
+ /** Bump when the critique request/response contract changes in a way that
4
+ * invalidates persisted findings (rubric semantics, finding schema, tiling
5
+ * geometry meaning). Part of every cache key and persisted critique. */
6
+ export declare const QA_CRITIQUE_CONTRACT_VERSION = 1;
7
+ /** Default Path B mismatch-ratio threshold. Strict on purpose: reuse trades
8
+ * essentially nothing for robustness against rasterization noise, and a
9
+ * looser threshold could hide a sub-threshold real change. */
10
+ export declare const DEFAULT_REUSE_MISMATCH_RATIO = 0.001;
11
+ /** Critique results persisted beside a QA snapshot, enough to decide reuse
12
+ * later: findings plus the geometry they were judged under. */
13
+ export interface PersistedCritique {
14
+ contract_version: number;
15
+ /** fnv1a32 of the exact rubric text the findings were judged under. */
16
+ rubric_digest: string;
17
+ outcome: "pass" | "fail";
18
+ findings: CritiqueFinding[];
19
+ tiles: Array<{
20
+ index: number;
21
+ label: string;
22
+ x: number;
23
+ scrollY: number;
24
+ width: number;
25
+ height: number;
26
+ }>;
27
+ }
28
+ /** Versioned, canonicalized Path A cache key (observe mode). Field order is
29
+ * part of the contract — never reorder without bumping the version. */
30
+ export declare function qaTileCacheKey(parts: {
31
+ tilePngSha256: string;
32
+ rubricDigest: string;
33
+ provider: string;
34
+ model: string;
35
+ viewport: string;
36
+ deviceScaleFactor: number;
37
+ theme: string;
38
+ state: string;
39
+ tileLabel: string;
40
+ browserVersion?: string;
41
+ }): string;
42
+ export declare function sha256Hex(buf: Buffer | Uint8Array): string;
43
+ export declare function rubricDigest(rubric: string): string;
44
+ /**
45
+ * Pixel-compare one document-space rect across two full-page screenshots.
46
+ * `sizeMismatch` is true when the rect isn't fully comparable in both images
47
+ * (page grew/shrank past it) — the caller treats that as changed.
48
+ */
49
+ export declare function rectMismatch(baselineFullPage: PNG, currentFullPage: PNG, rect: {
50
+ x: number;
51
+ y: number;
52
+ width: number;
53
+ height: number;
54
+ }, opts?: {
55
+ pixelThreshold?: number;
56
+ }): {
57
+ ratio: number;
58
+ comparablePixels: number;
59
+ sizeMismatch: boolean;
60
+ };
61
+ export interface TileReuseDecision {
62
+ index: number;
63
+ label: string;
64
+ reuse: boolean;
65
+ reason: string;
66
+ /** Rect mismatch ratio, present when a pixel comparison ran. */
67
+ mismatch_ratio?: number;
68
+ }
69
+ export interface CritiqueReusePlan {
70
+ mode: "band-diff";
71
+ /** Tiles that must be reviewed fresh, in original order. */
72
+ review: CritiqueTile[];
73
+ decisions: TileReuseDecision[];
74
+ tiles_total: number;
75
+ tiles_reused: number;
76
+ tiles_reviewed: number;
77
+ provider_calls_avoided: number;
78
+ mismatch_threshold: number;
79
+ /** Set when the whole baseline was unusable (everything reviews fresh). */
80
+ invalidation?: string;
81
+ }
82
+ /**
83
+ * Path B reuse plan: decide per tile whether the baseline run's verdict still
84
+ * covers it. A tile is reused ONLY when (a) the baseline is valid for this
85
+ * rubric and contract, (b) no baseline finding overlaps the tile's document
86
+ * region (a prior failure is never converted into a pass), and (c) its rect
87
+ * pixel-matches the baseline screenshot at or below the strict threshold.
88
+ * Everything else reviews fresh — reuse can produce false REVIEWS, never
89
+ * false passes.
90
+ */
91
+ export declare function planCritiqueReuse(args: {
92
+ baselineScreenshot: Buffer;
93
+ baselineCritique: PersistedCritique;
94
+ currentScreenshot: Buffer;
95
+ tiles: CritiqueTile[];
96
+ rubric: string;
97
+ mismatchThreshold?: number;
98
+ }): CritiqueReusePlan;
99
+ //# sourceMappingURL=qa-reuse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qa-reuse.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/qa-reuse.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGnE;;wEAEwE;AACxE,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C;;8DAE8D;AAC9D,eAAO,MAAM,4BAA4B,QAAQ,CAAC;AAElD;+DAC+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,CAAC,EAAE,MAAM,CAAC;QACV,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;CACJ;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAeT;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAE1D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD;AAmBD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,gBAAgB,EAAE,GAAG,EACrB,eAAe,EAAE,GAAG,EACpB,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7D,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACrC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAYpE;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,4DAA4D;IAC5D,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,iBAAiB,CAsGpB"}
@@ -0,0 +1,222 @@
1
+ // Visual-result reuse for critique tiles (plan 3.3): avoid re-judging pixels
2
+ // that did not change.
3
+ //
4
+ // Two mechanisms share this module; which is primary was decided by the
5
+ // Stage 0 byte-stability experiment, not preference:
6
+ //
7
+ // Path B (primary): band-level pixel diff. Compare each candidate tile's
8
+ // document-space rect in the CURRENT full-page screenshot against the SAME
9
+ // rect in the persisted baseline screenshot (qa-snapshot.ts), with
10
+ // pixelmatch's anti-aliasing tolerance. At or below a strict mismatch
11
+ // ratio (default 0.001), the tile region provably didn't change and the
12
+ // provider call is skipped. No byte identity required, so font
13
+ // rasterization and raster-path drift don't defeat it.
14
+ //
15
+ // Path A (observe mode: key computation + logging only): the Stage 0
16
+ // byte-stability experiment PASSED its gate (100% cross-restart identical
17
+ // bands on real pages, same-day/same-machine, deterministic settle), so an
18
+ // exact-digest cache is viable — but Path B subsumes its win (a
19
+ // byte-identical band pixel-diffs at ratio 0) with zero extra storage, so
20
+ // only the versioned cache key ships until telemetry shows a payoff Path B
21
+ // misses (e.g. cross-target tile sharing).
22
+ //
23
+ // Hard rules, both paths: never convert a prior failure into a pass — any
24
+ // baseline finding overlapping a tile's region forces a fresh review; a
25
+ // missing/invalid baseline, changed rubric, or changed critique contract is
26
+ // a miss, never an error.
27
+ import { createHash } from "node:crypto";
28
+ import pixelmatch from "pixelmatch";
29
+ import { PNG } from "pngjs";
30
+ import { fnv1a32 } from "./qa-plan.js";
31
+ /** Bump when the critique request/response contract changes in a way that
32
+ * invalidates persisted findings (rubric semantics, finding schema, tiling
33
+ * geometry meaning). Part of every cache key and persisted critique. */
34
+ export const QA_CRITIQUE_CONTRACT_VERSION = 1;
35
+ /** Default Path B mismatch-ratio threshold. Strict on purpose: reuse trades
36
+ * essentially nothing for robustness against rasterization noise, and a
37
+ * looser threshold could hide a sub-threshold real change. */
38
+ export const DEFAULT_REUSE_MISMATCH_RATIO = 0.001;
39
+ /** Versioned, canonicalized Path A cache key (observe mode). Field order is
40
+ * part of the contract — never reorder without bumping the version. */
41
+ export function qaTileCacheKey(parts) {
42
+ const canonical = [
43
+ `v${QA_CRITIQUE_CONTRACT_VERSION}`,
44
+ `png:${parts.tilePngSha256}`,
45
+ `rubric:${parts.rubricDigest}`,
46
+ `provider:${parts.provider}`,
47
+ `model:${parts.model}`,
48
+ `viewport:${parts.viewport}`,
49
+ `dpr:${parts.deviceScaleFactor}`,
50
+ `theme:${parts.theme}`,
51
+ `state:${parts.state}`,
52
+ `tile:${parts.tileLabel}`,
53
+ `browser:${parts.browserVersion ?? "unknown"}`,
54
+ ].join("\x1f");
55
+ return `qa1-${createHash("sha256").update(canonical).digest("hex")}`;
56
+ }
57
+ export function sha256Hex(buf) {
58
+ return createHash("sha256").update(buf).digest("hex");
59
+ }
60
+ export function rubricDigest(rubric) {
61
+ // Whitespace-normalized so formatting-only edits don't invalidate reuse.
62
+ return fnv1a32(rubric.replace(/\s+/g, " ").trim());
63
+ }
64
+ function cropRegion(png, rect) {
65
+ const sx = Math.max(0, Math.min(Math.round(rect.x), png.width - 1));
66
+ const sy = Math.max(0, Math.min(Math.round(rect.y), png.height - 1));
67
+ const w = Math.min(Math.round(rect.width), png.width - sx);
68
+ const h = Math.min(Math.round(rect.height), png.height - sy);
69
+ if (w <= 0 || h <= 0)
70
+ return null;
71
+ const out = new PNG({ width: w, height: h });
72
+ for (let row = 0; row < h; row++) {
73
+ const srcStart = ((sy + row) * png.width + sx) * 4;
74
+ png.data.copy(out.data, row * w * 4, srcStart, srcStart + w * 4);
75
+ }
76
+ return out;
77
+ }
78
+ /**
79
+ * Pixel-compare one document-space rect across two full-page screenshots.
80
+ * `sizeMismatch` is true when the rect isn't fully comparable in both images
81
+ * (page grew/shrank past it) — the caller treats that as changed.
82
+ */
83
+ export function rectMismatch(baselineFullPage, currentFullPage, rect, opts = {}) {
84
+ const base = cropRegion(baselineFullPage, rect);
85
+ const cur = cropRegion(currentFullPage, rect);
86
+ if (!base || !cur || base.width !== cur.width || base.height !== cur.height) {
87
+ return { ratio: 1, comparablePixels: 0, sizeMismatch: true };
88
+ }
89
+ const mismatched = pixelmatch(base.data, cur.data, undefined, base.width, base.height, {
90
+ threshold: opts.pixelThreshold ?? 0.1,
91
+ includeAA: false,
92
+ });
93
+ const total = base.width * base.height;
94
+ return { ratio: mismatched / total, comparablePixels: total, sizeMismatch: false };
95
+ }
96
+ function wholeRunMiss(tiles, threshold, invalidation) {
97
+ return {
98
+ mode: "band-diff",
99
+ review: tiles,
100
+ decisions: tiles.map((t) => ({
101
+ index: t.index,
102
+ label: t.label,
103
+ reuse: false,
104
+ reason: invalidation,
105
+ })),
106
+ tiles_total: tiles.length,
107
+ tiles_reused: 0,
108
+ tiles_reviewed: tiles.length,
109
+ provider_calls_avoided: 0,
110
+ mismatch_threshold: threshold,
111
+ invalidation,
112
+ };
113
+ }
114
+ /**
115
+ * Path B reuse plan: decide per tile whether the baseline run's verdict still
116
+ * covers it. A tile is reused ONLY when (a) the baseline is valid for this
117
+ * rubric and contract, (b) no baseline finding overlaps the tile's document
118
+ * region (a prior failure is never converted into a pass), and (c) its rect
119
+ * pixel-matches the baseline screenshot at or below the strict threshold.
120
+ * Everything else reviews fresh — reuse can produce false REVIEWS, never
121
+ * false passes.
122
+ */
123
+ export function planCritiqueReuse(args) {
124
+ const threshold = args.mismatchThreshold ?? DEFAULT_REUSE_MISMATCH_RATIO;
125
+ const { baselineCritique, tiles } = args;
126
+ if (baselineCritique.outcome !== "pass") {
127
+ return wholeRunMiss(tiles, threshold, "baseline critique did not pass");
128
+ }
129
+ if (baselineCritique.contract_version !== QA_CRITIQUE_CONTRACT_VERSION) {
130
+ return wholeRunMiss(tiles, threshold, `baseline critique contract v${baselineCritique.contract_version} != v${QA_CRITIQUE_CONTRACT_VERSION}`);
131
+ }
132
+ if (baselineCritique.rubric_digest !== rubricDigest(args.rubric)) {
133
+ return wholeRunMiss(tiles, threshold, "rubric changed since the baseline run");
134
+ }
135
+ let baselinePng;
136
+ let currentPng;
137
+ try {
138
+ baselinePng = PNG.sync.read(args.baselineScreenshot);
139
+ currentPng = PNG.sync.read(args.currentScreenshot);
140
+ }
141
+ catch {
142
+ return wholeRunMiss(tiles, threshold, "unreadable baseline or current screenshot");
143
+ }
144
+ // Document regions of baseline tiles that produced findings.
145
+ const findingTiles = new Set(baselineCritique.findings.map((f) => f.tile));
146
+ const dirtyRegions = baselineCritique.tiles
147
+ .filter((t) => findingTiles.has(t.index))
148
+ .map((t) => ({ top: t.scrollY, bottom: t.scrollY + t.height }));
149
+ const decisions = [];
150
+ const review = [];
151
+ for (const tile of tiles) {
152
+ const top = tile.scrollY;
153
+ const bottom = tile.scrollY + tile.height;
154
+ const left = tile.x ?? 0;
155
+ const right = left + tile.width;
156
+ const coverage = baselineCritique.tiles
157
+ .filter((prior) => prior.x <= left && prior.x + prior.width >= right)
158
+ .map((prior) => ({ top: prior.scrollY, bottom: prior.scrollY + prior.height }))
159
+ .sort((a, b) => a.top - b.top);
160
+ let coveredTo = top;
161
+ for (const region of coverage) {
162
+ if (region.bottom <= coveredTo || region.top > coveredTo)
163
+ continue;
164
+ coveredTo = Math.max(coveredTo, region.bottom);
165
+ if (coveredTo >= bottom)
166
+ break;
167
+ }
168
+ if (coveredTo < bottom) {
169
+ decisions.push({
170
+ index: tile.index,
171
+ label: tile.label,
172
+ reuse: false,
173
+ reason: "baseline critique did not cover this entire region",
174
+ });
175
+ review.push(tile);
176
+ continue;
177
+ }
178
+ const overlapsFinding = dirtyRegions.some((r) => r.top < bottom && r.bottom > top);
179
+ if (overlapsFinding) {
180
+ decisions.push({
181
+ index: tile.index,
182
+ label: tile.label,
183
+ reuse: false,
184
+ reason: "baseline run had findings in this region — always re-reviewed",
185
+ });
186
+ review.push(tile);
187
+ continue;
188
+ }
189
+ const rect = { x: tile.x ?? 0, y: tile.scrollY, width: tile.width, height: tile.height };
190
+ const cmp = rectMismatch(baselinePng, currentPng, rect);
191
+ if (cmp.sizeMismatch || cmp.ratio > threshold) {
192
+ decisions.push({
193
+ index: tile.index,
194
+ label: tile.label,
195
+ reuse: false,
196
+ reason: cmp.sizeMismatch
197
+ ? "region not comparable across screenshots (layout size changed)"
198
+ : `mismatch ratio ${cmp.ratio.toFixed(6)} > ${threshold}`,
199
+ mismatch_ratio: cmp.sizeMismatch ? undefined : cmp.ratio,
200
+ });
201
+ review.push(tile);
202
+ continue;
203
+ }
204
+ decisions.push({
205
+ index: tile.index,
206
+ label: tile.label,
207
+ reuse: true,
208
+ reason: `clean region pixel-matched baseline (ratio ${cmp.ratio.toFixed(6)})`,
209
+ mismatch_ratio: cmp.ratio,
210
+ });
211
+ }
212
+ return {
213
+ mode: "band-diff",
214
+ review,
215
+ decisions,
216
+ tiles_total: tiles.length,
217
+ tiles_reused: tiles.length - review.length,
218
+ tiles_reviewed: review.length,
219
+ provider_calls_avoided: tiles.length - review.length,
220
+ mismatch_threshold: threshold,
221
+ };
222
+ }