harnery 0.45.1 → 0.46.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 (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -36,7 +36,7 @@ export interface PlaceholderHit {
36
36
  export interface PlaceholderResult {
37
37
  rule: "placeholder";
38
38
  found: boolean;
39
- outcome: "pass" | "fail";
39
+ outcome: "pass" | "fail" | "unknown";
40
40
  scanned: number;
41
41
  truncated: boolean;
42
42
  hits: PlaceholderHit[];
@@ -100,6 +100,8 @@ export interface ContrastResult {
100
100
  }
101
101
 
102
102
  export interface ContentChecksRequest {
103
+ /** Explicit traversal budget; omission retains the existing node cap. */
104
+ scanNodeBudget?: number;
103
105
  placeholder: { scope: string | null } | null;
104
106
  image: { scope: string | null; tolerance: number } | null;
105
107
  truncation: { scope: string | null; tolerance: number } | null;
@@ -115,7 +117,15 @@ export interface ContentChecksResult {
115
117
 
116
118
  export function buildContentChecks(): (request: ContentChecksRequest) => ContentChecksResult {
117
119
  return (request) => {
118
- const NODE_CAP = 6000;
120
+ if (
121
+ request.scanNodeBudget !== undefined &&
122
+ (!Number.isSafeInteger(request.scanNodeBudget) ||
123
+ request.scanNodeBudget < 1 ||
124
+ request.scanNodeBudget > 100_000)
125
+ ) {
126
+ throw new Error("scanNodeBudget must be an integer from 1 to 100000.");
127
+ }
128
+ const NODE_CAP = request.scanNodeBudget ?? 6000;
119
129
  const HIT_CAP = 100;
120
130
 
121
131
  const sx = window.scrollX;
@@ -234,6 +244,7 @@ export function buildContentChecks(): (request: ContentChecksRequest) => Content
234
244
  // when the trimmed text of a leaf element is exactly the tell).
235
245
  if (hits.length < HIT_CAP) {
236
246
  const leaves = root.querySelectorAll("*");
247
+ if (leaves.length > NODE_CAP) truncated = true;
237
248
  for (let i = 0; i < leaves.length && i < NODE_CAP; i++) {
238
249
  const el = leaves[i];
239
250
  if (!el || el.children.length > 0) continue;
@@ -258,7 +269,7 @@ export function buildContentChecks(): (request: ContentChecksRequest) => Content
258
269
  placeholder = {
259
270
  rule: "placeholder",
260
271
  found: root !== null,
261
- outcome: root && hits.length === 0 ? "pass" : "fail",
272
+ outcome: !root || hits.length > 0 ? "fail" : truncated ? "unknown" : "pass",
262
273
  scanned,
263
274
  truncated,
264
275
  hits,
@@ -18,6 +18,7 @@
18
18
  // orchestration live here; the model call lives in the host.
19
19
 
20
20
  import { PNG } from "pngjs";
21
+ import { PAGE_REVIEW_MAX_TILE_BUDGET } from "./page-review-contracts.js";
21
22
  import {
22
23
  bandOversizedRect,
23
24
  bandsCoveringRects,
@@ -76,6 +77,87 @@ export interface CritiqueResult {
76
77
  error?: string;
77
78
  /** Host-reported provider provenance (route, attempts, fallbacks, usage). */
78
79
  provider_meta?: Record<string, unknown>;
80
+ /** Tokens the provider reported across every tile it was called for. */
81
+ usage?: CritiqueUsageTotals;
82
+ }
83
+
84
+ /** Tokens one provider call spent, as the host's backend reported them.
85
+ * `input_tokens` counts every input token, cached or not. */
86
+ export interface CritiqueUsage {
87
+ input_tokens: number;
88
+ output_tokens: number;
89
+ cache_read_input_tokens?: number;
90
+ cache_write_input_tokens?: number;
91
+ /** Cost at the backend's list price, when the backend reports one. */
92
+ cost_usd?: number;
93
+ }
94
+
95
+ /** Usage summed over tiles. A tile whose provider reported nothing counts in
96
+ * `tiles_unreported`, never as zero tokens. */
97
+ export interface CritiqueUsageTotals {
98
+ tiles_reported: number;
99
+ tiles_unreported: number;
100
+ input_tokens: number;
101
+ output_tokens: number;
102
+ cache_read_input_tokens: number;
103
+ cache_write_input_tokens: number;
104
+ /** Present only when every reported tile carried a cost. */
105
+ cost_usd?: number;
106
+ }
107
+
108
+ /** Add one call's usage to a tile's running usage (a retried tile reports twice). */
109
+ export function addCritiqueUsage(
110
+ into: CritiqueUsage | undefined,
111
+ usage: CritiqueUsage,
112
+ ): CritiqueUsage {
113
+ const n = (v: number | undefined) => (Number.isFinite(v) ? (v as number) : 0);
114
+ const out: CritiqueUsage = {
115
+ input_tokens: n(into?.input_tokens) + n(usage.input_tokens),
116
+ output_tokens: n(into?.output_tokens) + n(usage.output_tokens),
117
+ };
118
+ const read = n(into?.cache_read_input_tokens) + n(usage.cache_read_input_tokens);
119
+ const write = n(into?.cache_write_input_tokens) + n(usage.cache_write_input_tokens);
120
+ if (read > 0) out.cache_read_input_tokens = read;
121
+ if (write > 0) out.cache_write_input_tokens = write;
122
+ const costKnown =
123
+ usage.cost_usd !== undefined && (into === undefined || into.cost_usd !== undefined);
124
+ if (costKnown) out.cost_usd = n(into?.cost_usd) + n(usage.cost_usd);
125
+ return out;
126
+ }
127
+
128
+ export function sumCritiqueUsage(
129
+ perTile: ReadonlyArray<CritiqueUsage | undefined>,
130
+ ): CritiqueUsageTotals {
131
+ const reported = perTile.filter((u): u is CritiqueUsage => u !== undefined);
132
+ const totals: CritiqueUsageTotals = {
133
+ tiles_reported: reported.length,
134
+ tiles_unreported: perTile.length - reported.length,
135
+ input_tokens: 0,
136
+ output_tokens: 0,
137
+ cache_read_input_tokens: 0,
138
+ cache_write_input_tokens: 0,
139
+ };
140
+ for (const u of reported) {
141
+ totals.input_tokens += u.input_tokens;
142
+ totals.output_tokens += u.output_tokens;
143
+ totals.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
144
+ totals.cache_write_input_tokens += u.cache_write_input_tokens ?? 0;
145
+ }
146
+ if (reported.length > 0 && reported.every((u) => u.cost_usd !== undefined)) {
147
+ totals.cost_usd =
148
+ Math.round(reported.reduce((sum, u) => sum + (u.cost_usd ?? 0), 0) * 1e6) / 1e6;
149
+ }
150
+ return totals;
151
+ }
152
+
153
+ /** "1,234,567 tokens over 96 tiles (3 unreported), $1.23" for log lines. */
154
+ export function formatCritiqueUsage(totals: CritiqueUsageTotals): string {
155
+ const tokens = (totals.input_tokens + totals.output_tokens).toLocaleString("en-US");
156
+ return (
157
+ `${tokens} tokens over ${totals.tiles_reported} tile(s)` +
158
+ (totals.tiles_unreported > 0 ? ` (${totals.tiles_unreported} unreported)` : "") +
159
+ (totals.cost_usd !== undefined ? `, $${totals.cost_usd.toFixed(2)} at list price` : "")
160
+ );
79
161
  }
80
162
 
81
163
  /**
@@ -93,6 +175,9 @@ export type CritiqueProvider = ((input: {
93
175
  url: string;
94
176
  rubric: string;
95
177
  tile: CritiqueTile;
178
+ /** Optional sink for the tokens this call spent. A provider that knows its
179
+ * usage calls it once per backend call; callers that omit it lose nothing. */
180
+ reportUsage?: (usage: CritiqueUsage) => void;
96
181
  }) => Promise<CritiqueFinding[]>) & {
97
182
  concurrency?: number;
98
183
  meta?: () => Record<string, unknown> | undefined;
@@ -242,12 +327,16 @@ export function tilesFromFullPage(
242
327
  pngBase64: PNG.sync.write(dst).toString("base64"),
243
328
  };
244
329
  };
245
- const kept = rects.slice(0, opts.maxTiles);
330
+ const kept = rects.slice(0, Math.min(opts.maxTiles, PAGE_REVIEW_MAX_TILE_BUDGET));
246
331
  const tiles: CritiqueTile[] = kept.map(cutTile);
332
+ const hitBandCap = Math.min(
333
+ opts.maxHitBands ?? 50,
334
+ Math.max(0, PAGE_REVIEW_MAX_TILE_BUDGET - kept.length),
335
+ );
247
336
  let hitBands = 0;
248
337
  const banding = !(opts.elementRects && opts.elementRects.length > 0);
249
338
  if (banding && opts.hitRects && opts.hitRects.length > 0) {
250
- for (const index of bandsCoveringRects(rects, kept.length, opts.hitRects, opts.maxHitBands)) {
339
+ for (const index of bandsCoveringRects(rects, kept.length, opts.hitRects, hitBandCap)) {
251
340
  const band = rects[index];
252
341
  if (!band) continue;
253
342
  tiles.push(cutTile({ ...band, label: `hit band ${index + 1}` }));
@@ -323,10 +412,23 @@ export async function runCritique(args: {
323
412
  "no critiqueProvider injected by the host (see HarneryProgramContext.critiqueProvider)",
324
413
  };
325
414
  }
415
+ if (tiles.length > PAGE_REVIEW_MAX_TILE_BUDGET) {
416
+ return {
417
+ rule: "critique",
418
+ tiles: tiles.length,
419
+ provider: true,
420
+ findings: [],
421
+ outcome: "skipped",
422
+ error:
423
+ `${tiles.length} tiles exceed the per-invocation ceiling of ${PAGE_REVIEW_MAX_TILE_BUDGET} ` +
424
+ "vision calls; lower the tile cap",
425
+ };
426
+ }
326
427
  // Tiles are judged independently, so run them concurrently (bounded) and
327
428
  // reassemble findings in tile order so artifacts stay deterministic.
328
429
  const concurrency = Math.max(1, Math.min(provider.concurrency ?? 4, tiles.length || 1));
329
430
  const perTile: CritiqueFinding[][] = tiles.map(() => []);
431
+ const perTileUsage: Array<CritiqueUsage | undefined> = tiles.map(() => undefined);
330
432
  let cursor = 0;
331
433
  const worker = async (): Promise<void> => {
332
434
  while (true) {
@@ -334,7 +436,14 @@ export async function runCritique(args: {
334
436
  if (i >= tiles.length) return;
335
437
  const tile = tiles[i];
336
438
  try {
337
- const tileFindings = await provider({ url, rubric, tile });
439
+ const tileFindings = await provider({
440
+ url,
441
+ rubric,
442
+ tile,
443
+ reportUsage: (usage) => {
444
+ perTileUsage[i] = addCritiqueUsage(perTileUsage[i], usage);
445
+ },
446
+ });
338
447
  perTile[i] = tileFindings.map((f) => ({ ...f, tile: tile.index }));
339
448
  } catch (err: unknown) {
340
449
  perTile[i] = [
@@ -359,5 +468,6 @@ export async function runCritique(args: {
359
468
  findings,
360
469
  outcome,
361
470
  ...(meta ? { provider_meta: meta } : {}),
471
+ usage: sumCritiqueUsage(perTileUsage),
362
472
  };
363
473
  }
@@ -151,6 +151,8 @@ export interface CrowdResult {
151
151
  }
152
152
 
153
153
  export interface LayoutLintRequest {
154
+ /** Explicit traversal budget; omission retains each rule's existing limit. */
155
+ scanNodeBudget?: number;
154
156
  align: Array<{ selector: string; axis: LayoutAxis; tolerancePx: number }>;
155
157
  gap: Array<{
156
158
  selector: string;
@@ -204,9 +206,26 @@ export function intersectRects(a: LayoutRect, b: LayoutRect): LayoutRect | null
204
206
  */
205
207
  export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLintResult {
206
208
  return (request) => {
207
- const CHILD_LIMIT = 1_000;
208
- const TEXT_LIMIT = 1_000;
209
+ if (
210
+ request.scanNodeBudget !== undefined &&
211
+ (!Number.isSafeInteger(request.scanNodeBudget) ||
212
+ request.scanNodeBudget < 1 ||
213
+ request.scanNodeBudget > 100_000)
214
+ ) {
215
+ throw new Error("scanNodeBudget must be an integer from 1 to 100000.");
216
+ }
217
+ const CHILD_LIMIT = request.scanNodeBudget ?? 1_000;
218
+ const TEXT_LIMIT = request.scanNodeBudget ?? 1_000;
209
219
  const ISSUE_LIMIT = 100;
220
+ const translationOnly = (transform: string): boolean => {
221
+ if (!transform || transform === "none") return true;
222
+ try {
223
+ const matrix = new DOMMatrixReadOnly(transform);
224
+ return matrix.is2D && matrix.a === 1 && matrix.b === 0 && matrix.c === 0 && matrix.d === 1;
225
+ } catch {
226
+ return false;
227
+ }
228
+ };
210
229
  // Chromium geometry is fractional even when authored CSS is integral.
211
230
  // Differences below half a CSS pixel are rasterization noise, not clipping.
212
231
  const SUBPIXEL_EPSILON = 0.5;
@@ -673,17 +692,22 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
673
692
  container: Element,
674
693
  seed: LayoutRect,
675
694
  seedLabel: string,
676
- ): { allowed: LayoutRect; clippedBy: { x: string; y: string } } => {
695
+ ): { allowed: LayoutRect; clippedBy: { x: string; y: string }; supported: boolean } => {
677
696
  let allowed = { ...seed };
697
+ let supported = true;
678
698
  const clippedBy = { x: seedLabel, y: seedLabel };
679
699
  const free = { x: false, y: false };
680
700
  let current: Element | null = start;
681
701
  while (current && container.contains(current)) {
682
702
  const style = getComputedStyle(current);
683
- if (style.clipPath && style.clipPath !== "none")
703
+ if (style.clipPath && style.clipPath !== "none") {
684
704
  unsupported.add(`${labelOf(current)}:clip-path`);
685
- if (style.transform && style.transform !== "none")
705
+ supported = false;
706
+ }
707
+ if (!translationOnly(style.transform)) {
686
708
  unsupported.add(`${labelOf(current)}:transform`);
709
+ supported = false;
710
+ }
687
711
  // The container bounds by default even when it does not CSS-clip,
688
712
  // since it is the scope the caller asked about -- but a scroller
689
713
  // hands its axis back whether it is the scope or an ancestor.
@@ -727,7 +751,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
727
751
  if (scope) break;
728
752
  current = current.parentElement;
729
753
  }
730
- return { allowed, clippedBy };
754
+ return { allowed, clippedBy, supported };
731
755
  };
732
756
 
733
757
  const nearestBlockOwner = (node: Node, scope: Element): Element | null => {
@@ -755,6 +779,15 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
755
779
  excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "visually-hidden" });
756
780
  return;
757
781
  }
782
+ const ownStyle = getComputedStyle(element);
783
+ if (!translationOnly(ownStyle.transform)) {
784
+ unsupported.add(`${labelOf(element)}:transform`);
785
+ return;
786
+ }
787
+ if (ownStyle.clipPath && ownStyle.clipPath !== "none") {
788
+ unsupported.add(`${labelOf(element)}:clip-path`);
789
+ return;
790
+ }
758
791
  const rect = element.getBoundingClientRect();
759
792
  if (rect.width <= 0 || rect.height <= 0) {
760
793
  excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
@@ -783,7 +816,8 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
783
816
  seedLabel = labelOf(owner);
784
817
  }
785
818
  const chain = clipChain(element.parentElement, container, seed, seedLabel);
786
- recordIssue(measure(element, index, false), chain.allowed, chain.clippedBy);
819
+ if (chain.supported)
820
+ recordIssue(measure(element, index, false), chain.allowed, chain.clippedBy);
787
821
  });
788
822
 
789
823
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
@@ -825,7 +859,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
825
859
  let current: Element | null = owner;
826
860
  while (current && container.contains(current)) {
827
861
  const transform = getComputedStyle(current).transform;
828
- if (transform && transform !== "none") {
862
+ if (!translationOnly(transform)) {
829
863
  unsupported.add(`${labelOf(current)}:transform`);
830
864
  transformed = true;
831
865
  break;
@@ -851,6 +885,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
851
885
  seed.width = Math.max(0, seed.right - seed.left);
852
886
  }
853
887
  const chain = clipChain(owner, container, seed, labelOf(owner));
888
+ if (!chain.supported) continue;
854
889
  const allowed = chain.allowed;
855
890
  const clippedBy = chain.clippedBy;
856
891
  const range = document.createRange();
@@ -877,7 +912,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
877
912
  // Border radius keeps the rectangular padding-box contract; only shapes
878
913
  // the rectangle cannot certify contribute to unsupported geometry.
879
914
  const outcome: LayoutOutcome =
880
- issues.length > 0 ? "fail" : unsupported.size > 0 ? "unknown" : "pass";
915
+ issues.length > 0 ? "fail" : unsupported.size > 0 || truncated ? "unknown" : "pass";
881
916
  return {
882
917
  rule: "clip" as const,
883
918
  selector,
@@ -966,7 +1001,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
966
1001
  // and badges (radius ≥ half their smaller side) are inline controls, not
967
1002
  // layout cards, and are excluded too. Plain blocks with no panel chrome
968
1003
  // and no panel descendants stay out of the peer list.
969
- const PARENT_LIMIT = 2000;
1004
+ const PARENT_LIMIT = request.scanNodeBudget ?? 2000;
970
1005
  const PANEL_DESCENDANT_LIMIT = 100;
971
1006
  const OVERLAP_EPS = 1;
972
1007
  const STRUCTURAL_TAGS = new Set([
@@ -1036,13 +1071,18 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
1036
1071
  const collectPanelFaces = (
1037
1072
  element: Element,
1038
1073
  selfIsPanel: boolean,
1074
+ onTruncated: () => void,
1039
1075
  ): LayoutElementMeasurement[] => {
1040
1076
  if (selfIsPanel) return [measure(element, 0, false)];
1041
1077
  const faces: LayoutElementMeasurement[] = [];
1042
1078
  const descendants = element.querySelectorAll("*");
1043
- for (let i = 0; i < descendants.length && faces.length < PANEL_DESCENDANT_LIMIT; i++) {
1079
+ for (let i = 0; i < descendants.length; i++) {
1044
1080
  const child = descendants[i];
1045
1081
  if (!child || !isInFlowBox(child) || !isPanel(child)) continue;
1082
+ if (faces.length >= PANEL_DESCENDANT_LIMIT) {
1083
+ onTruncated();
1084
+ break;
1085
+ }
1046
1086
  faces.push(measure(child, faces.length, false));
1047
1087
  }
1048
1088
  return faces;
@@ -1054,6 +1094,7 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
1054
1094
  // panels separated only by chrome still get a face-to-face check.
1055
1095
  const collectCrowdPeers = (
1056
1096
  parent: Element,
1097
+ onTruncated: () => void,
1057
1098
  ): Array<{
1058
1099
  kind: "panel" | "composite";
1059
1100
  faces: LayoutElementMeasurement[];
@@ -1063,11 +1104,12 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
1063
1104
  faces: LayoutElementMeasurement[];
1064
1105
  }> = [];
1065
1106
  const children = [...parent.children];
1107
+ if (children.length > CHILD_LIMIT) onTruncated();
1066
1108
  for (let index = 0; index < children.length && index < CHILD_LIMIT; index++) {
1067
1109
  const element = children[index];
1068
1110
  if (!element || !isInFlowBox(element)) continue;
1069
1111
  const selfIsPanel = isPanel(element);
1070
- const faces = collectPanelFaces(element, selfIsPanel);
1112
+ const faces = collectPanelFaces(element, selfIsPanel, onTruncated);
1071
1113
  if (faces.length === 0) continue;
1072
1114
  out.push({
1073
1115
  kind: selfIsPanel ? "panel" : "composite",
@@ -1149,7 +1191,9 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
1149
1191
  const limit = Math.min(parents.length, PARENT_LIMIT);
1150
1192
  for (let p = 0; p < limit; p++) {
1151
1193
  const parent = parents[p]!;
1152
- const peers = collectCrowdPeers(parent);
1194
+ const peers = collectCrowdPeers(parent, () => {
1195
+ truncated = true;
1196
+ });
1153
1197
  for (let i = 1; i < peers.length; i++) {
1154
1198
  const beforePeer = peers[i - 1]!;
1155
1199
  const afterPeer = peers[i]!;
@@ -1178,7 +1222,12 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
1178
1222
  rule: "crowd" as const,
1179
1223
  selector,
1180
1224
  found: true,
1181
- outcome: issues.length > 0 ? ("fail" as const) : ("pass" as const),
1225
+ outcome:
1226
+ issues.length > 0
1227
+ ? ("fail" as const)
1228
+ : truncated
1229
+ ? ("unknown" as const)
1230
+ : ("pass" as const),
1182
1231
  minGapPx,
1183
1232
  issues,
1184
1233
  truncated,
@@ -38,15 +38,20 @@ export type {
38
38
  TruncationResult,
39
39
  } from "./content-checks.js";
40
40
  export {
41
+ addCritiqueUsage,
41
42
  bandRects,
42
43
  type CritiqueCoverage,
43
44
  type CritiqueFinding,
44
45
  type CritiqueProvider,
45
46
  type CritiqueResult,
46
47
  type CritiqueTile,
48
+ type CritiqueUsage,
49
+ type CritiqueUsageTotals,
47
50
  DEFAULT_CRITIQUE_RUBRIC,
51
+ formatCritiqueUsage,
48
52
  normalizeFindings,
49
53
  runCritique,
54
+ sumCritiqueUsage,
50
55
  tilesFromFullPage,
51
56
  } from "./critique.js";
52
57
  export {
@@ -6,7 +6,12 @@ export const PAGE_REVIEW_CAPTURE_PLAN_SCHEMA = "harnery-page-review-capture-plan
6
6
  export const PAGE_REVIEW_NATIVE_EVIDENCE_SCHEMA = "harnery-page-review-native-evidence/v2";
7
7
  export const PAGE_REVIEW_DECISIONS_SCHEMA = "harnery-page-review-decisions/v1";
8
8
  export const PAGE_REVIEW_DEFAULT_TILE_BUDGET = 96;
9
- export const PAGE_REVIEW_MAX_TILE_BUDGET = 400;
9
+ /** Hard ceiling on vision calls in one critique invocation (a qa-run pack,
10
+ * a review-pack judge, or a browse --check-critique). Each call is a separate
11
+ * model session, so the ceiling bounds what one run can spend whatever the
12
+ * job asks for. Pages that need more bands than this get a sampled review
13
+ * (ADR 0224). */
14
+ export const PAGE_REVIEW_MAX_TILE_BUDGET = 160;
10
15
 
11
16
  /** Rectangles are native image pixels, never CSS pixels or overview pixels. */
12
17
  export interface PageReviewRect {
@@ -9,8 +9,20 @@
9
9
  //
10
10
  // Toolkit tier: this module must not import src/core (layering check).
11
11
 
12
- import type { CritiqueFinding, CritiqueProvider, CritiqueTile } from "./critique.js";
13
- import type { PageReviewNativeEvidence } from "./page-review-contracts.js";
12
+ import {
13
+ addCritiqueUsage,
14
+ type CritiqueFinding,
15
+ type CritiqueProvider,
16
+ type CritiqueTile,
17
+ type CritiqueUsage,
18
+ type CritiqueUsageTotals,
19
+ formatCritiqueUsage,
20
+ sumCritiqueUsage,
21
+ } from "./critique.js";
22
+ import {
23
+ PAGE_REVIEW_MAX_TILE_BUDGET,
24
+ type PageReviewNativeEvidence,
25
+ } from "./page-review-contracts.js";
14
26
  import {
15
27
  buildPackNativeEvidence,
16
28
  PAGE_REVIEW_CRITIQUE_SCHEMA,
@@ -69,6 +81,8 @@ export interface JudgePageReviewPackResult {
69
81
  tiles_reused: number;
70
82
  outcome: "pass" | "fail" | "skipped" | "incomplete";
71
83
  provider_meta?: Record<string, unknown>;
84
+ /** Tokens summed over the tiles sent to the provider; absent when none were. */
85
+ usage?: CritiqueUsageTotals;
72
86
  }
73
87
 
74
88
  interface WorkItem {
@@ -262,9 +276,24 @@ export async function judgePageReviewPack(
262
276
  });
263
277
 
264
278
  let meta: Record<string, unknown> | undefined;
279
+ let usage: CritiqueUsageTotals | undefined;
265
280
  let concurrency = 0;
266
281
  const tileTimings: Array<QaRunCritiqueTileTiming | undefined> = new Array(work.length);
267
- if (provider && work.length > 0) {
282
+ // Filled, not sparse: filter() skips holes, which would drop unreported tiles.
283
+ const tileUsage: Array<CritiqueUsage | undefined> = Array.from(
284
+ { length: work.length },
285
+ () => undefined,
286
+ );
287
+ // Every vision call is a separate model session. A pack above the ceiling
288
+ // (an older pack, or one assembled by hand) is refused whole rather than
289
+ // judged in part, so no flag or pack can turn one judge into hundreds of calls.
290
+ const overCeiling =
291
+ work.length > PAGE_REVIEW_MAX_TILE_BUDGET
292
+ ? `pack needs ${work.length} vision calls, above the per-run ceiling of ` +
293
+ `${PAGE_REVIEW_MAX_TILE_BUDGET}; recapture it with a smaller tile budget`
294
+ : undefined;
295
+ if (overCeiling) log(`judge: refused — ${overCeiling}`);
296
+ if (provider && work.length > 0 && !overCeiling) {
268
297
  concurrency = Math.max(
269
298
  1,
270
299
  Math.min(options.concurrency ?? provider.concurrency ?? 4, work.length),
@@ -283,7 +312,14 @@ export async function judgePageReviewPack(
283
312
  const tileStarted = performance.now();
284
313
  let tileOutcome: QaRunCritiqueTileTiming["outcome"] = "passed";
285
314
  try {
286
- const found = await provider({ url: item.url, rubric: item.rubric, tile: item.tile });
315
+ const found = await provider({
316
+ url: item.url,
317
+ rubric: item.rubric,
318
+ tile: item.tile,
319
+ reportUsage: (usage) => {
320
+ tileUsage[i] = addCritiqueUsage(tileUsage[i], usage);
321
+ },
322
+ });
287
323
  for (const finding of found) {
288
324
  entry.findings.push({ ...finding, tile: item.tile.index, tile_id: item.tile.id });
289
325
  }
@@ -302,12 +338,16 @@ export async function judgePageReviewPack(
302
338
  tile_id: item.tile.id,
303
339
  duration_ms: Math.max(0, Math.round(performance.now() - tileStarted)),
304
340
  outcome: tileOutcome,
341
+ ...(tileUsage[i] ? { usage: tileUsage[i] } : {}),
305
342
  };
306
343
  }
307
344
  }
308
345
  };
309
346
  await Promise.all(Array.from({ length: concurrency }, () => worker()));
310
347
  meta = provider.meta?.();
348
+ // Only tiles the pool reached count; an unreached tile spent nothing.
349
+ usage = sumCritiqueUsage(tileUsage.filter((_, index) => tileTimings[index] !== undefined));
350
+ log(`judge: ${formatCritiqueUsage(usage)}`);
311
351
  } else if (provider) {
312
352
  meta = provider.meta?.();
313
353
  }
@@ -319,11 +359,13 @@ export async function judgePageReviewPack(
319
359
  const unjudged = entry.review.filter((tile) => !entry.judged.has(tile.id)).length;
320
360
  const outcome: JudgedContext["outcome"] = !provider
321
361
  ? "skipped"
322
- : findings.some((f) => f.severity === "high")
323
- ? "fail"
324
- : unjudged > 0 || entry.record.capture_incomplete
325
- ? "incomplete"
326
- : "pass";
362
+ : overCeiling
363
+ ? "incomplete"
364
+ : findings.some((f) => f.severity === "high")
365
+ ? "fail"
366
+ : unjudged > 0 || entry.record.capture_incomplete
367
+ ? "incomplete"
368
+ : "pass";
327
369
  return {
328
370
  context_id: entry.record.id,
329
371
  provider: provider ? label : "none",
@@ -334,12 +376,14 @@ export async function judgePageReviewPack(
334
376
  outcome,
335
377
  findings,
336
378
  coverage: entry.record.coverage,
337
- ...(provider
338
- ? {}
339
- : {
379
+ ...(!provider
380
+ ? {
340
381
  error:
341
382
  "no critiqueProvider injected by the host (see HarneryProgramContext.critiqueProvider)",
342
- }),
383
+ }
384
+ : overCeiling
385
+ ? { error: overCeiling }
386
+ : {}),
343
387
  record: entry.record,
344
388
  evidence: {
345
389
  schema: PAGE_REVIEW_CRITIQUE_SCHEMA,
@@ -381,6 +425,7 @@ export async function judgePageReviewPack(
381
425
  tiles_reused: tilesReused,
382
426
  outcome,
383
427
  ...(meta ? { provider_meta: meta } : {}),
428
+ ...(usage ? { usage } : {}),
384
429
  };
385
430
  }
386
431
 
@@ -328,6 +328,9 @@ export interface PageReviewCritiqueRecord {
328
328
  outcome: "pass" | "fail" | "skipped" | "incomplete";
329
329
  findings: Array<CritiqueFinding & { tile_id: string }>;
330
330
  coverage: CritiqueCoverage;
331
+ /** Set by a signoff whose complete review would exceed the per-run tile
332
+ * ceiling: the capped coverage is the accepted, sampled review (ADR 0224). */
333
+ coverage_mode?: "sampled";
331
334
  error?: string;
332
335
  evidence?: {
333
336
  schema: string;
@@ -1364,7 +1367,12 @@ export function resolvePackVerdict(
1364
1367
  (row.outcome === "skipped" && machine_outcome !== "skipped"),
1365
1368
  ) ??
1366
1369
  false) ||
1367
- manifest.contexts?.some((c) => c.capture_incomplete || c.coverage.capped) ||
1370
+ manifest.contexts?.some(
1371
+ (c) =>
1372
+ c.capture_incomplete ||
1373
+ (c.coverage.capped &&
1374
+ !critique?.some((row) => row.context_id === c.id && row.coverage_mode === "sampled")),
1375
+ ) ||
1368
1376
  false;
1369
1377
  const reviewed_outcome: PageReviewVerdict["reviewed_outcome"] =
1370
1378
  blocking > 0
@@ -2067,8 +2075,11 @@ export function renderReviewMarkdown(
2067
2075
  );
2068
2076
  lines.push("|---|---|---|---|---|---|---|---|---|");
2069
2077
  for (const ctx of contexts) {
2078
+ const sampled = manifest.critique?.some(
2079
+ (row) => row.context_id === ctx.id && row.coverage_mode === "sampled",
2080
+ );
2070
2081
  const cov = ctx.coverage.capped
2071
- ? `${ctx.coverage.bands_reviewed}/${ctx.coverage.bands_total} bands, capped`
2082
+ ? `${ctx.coverage.bands_reviewed}/${ctx.coverage.bands_total} bands, ${sampled ? "sampled" : "capped"}`
2072
2083
  : "complete";
2073
2084
  const contacts = ctx.contact_sheet
2074
2085
  ? `[${CONTACT_SHEET_FILENAME}](${ctx.contact_sheet.file})`