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.
- package/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/agents/state/pidmap.js +2 -2
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +2 -25
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +0 -2
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +3 -18
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/workflow/engine.js +2 -1
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +30 -74
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/agents/state/pidmap.ts +2 -2
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/config.ts +85 -0
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +2 -25
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +3 -23
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/workflow/engine.ts +1 -1
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/proof.ts +35 -83
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/core/agents/state/process-handle.ts +0 -165
- 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
|
-
|
|
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
|
|
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,
|
|
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({
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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,
|
package/src/lib/browser/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
13
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
:
|
|
323
|
-
? "
|
|
324
|
-
:
|
|
325
|
-
? "
|
|
326
|
-
:
|
|
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(
|
|
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})`
|