harnery 0.45.2 → 0.46.1
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/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/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- 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/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- 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/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- 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 +103 -5
- 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 +0 -23
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- 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/semantic/contract.d.ts +5 -5
- 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/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/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/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- 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/config.ts +85 -0
- 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/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +110 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/resolve/transcript.ts +61 -0
- 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/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- 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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type CookieJar,
|
|
16
16
|
CookieStoreParseError,
|
|
17
17
|
type Cookie as JarCookie,
|
|
18
|
+
violatesCookiePrefix,
|
|
18
19
|
} from "../cookies/index.ts";
|
|
19
20
|
import { PaceGate, pacePolicyFromEnv } from "../pace/index.ts";
|
|
20
21
|
import { type AssertResult, type AssertSpec, buildAssertCheck } from "./asserts.js";
|
|
@@ -170,8 +171,22 @@ export interface BrowserOptions {
|
|
|
170
171
|
userAgent?: string;
|
|
171
172
|
/** Authenticated browser proxy passed directly to Playwright. */
|
|
172
173
|
proxy?: { server: string; username?: string; password?: string };
|
|
174
|
+
/**
|
|
175
|
+
* Inspect the live cookies of the browser context. Runs before each request
|
|
176
|
+
* is sent, after each response, and once more at close. If it throws, the
|
|
177
|
+
* request is cancelled, the context closes without saving cookies to the
|
|
178
|
+
* jar, and the error is kept as `cookieGuardFault` for the caller to report.
|
|
179
|
+
*/
|
|
180
|
+
cookieGuard?: CookieGuard;
|
|
173
181
|
}
|
|
174
182
|
|
|
183
|
+
export type CookieGuardPhase = "request" | "response" | "close";
|
|
184
|
+
|
|
185
|
+
export type CookieGuard = (
|
|
186
|
+
cookies: readonly PWCookie[],
|
|
187
|
+
event: { phase: CookieGuardPhase; url: string | null },
|
|
188
|
+
) => void | Promise<void>;
|
|
189
|
+
|
|
175
190
|
export interface NavigateResult {
|
|
176
191
|
url: string;
|
|
177
192
|
title: string;
|
|
@@ -422,16 +437,35 @@ export class Browser {
|
|
|
422
437
|
: undefined;
|
|
423
438
|
if (identity) await this.context.addInitScript(installDocumentIdentity, identity);
|
|
424
439
|
|
|
425
|
-
|
|
426
|
-
|
|
440
|
+
// The jar's domain field is the delivery rule: a dotless domain is
|
|
441
|
+
// host-only, which Chromium honors for exactly that host. A cookie that
|
|
442
|
+
// breaks its name prefix would make Chromium reject the whole batch, so
|
|
443
|
+
// it is left out rather than aborting the session.
|
|
444
|
+
const injectable = jarCookies.filter((cookie) => !violatesCookiePrefix(cookie));
|
|
445
|
+
if (injectable.length > 0) {
|
|
446
|
+
await this.context.addCookies(injectable.map(toPWCookie));
|
|
427
447
|
}
|
|
428
448
|
|
|
429
449
|
// Gate actual document requests, including links, forms, script navigation,
|
|
430
450
|
// and the first request of a popup. Method-level waits miss those loads.
|
|
431
451
|
// Redirect hops are part of the original load, not additional reservations.
|
|
432
452
|
const headersCb = this.opts.extraHeaders;
|
|
433
|
-
|
|
453
|
+
const cookieGuard = this.opts.cookieGuard;
|
|
454
|
+
if (cookieGuard) {
|
|
455
|
+
this.context.on("response", (response) => {
|
|
456
|
+
void this.guardCookies("response", response.url()).catch(() => undefined);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (this.getPaceGate()?.policy.enabled || headersCb || this.opts.userAgent || cookieGuard) {
|
|
434
460
|
await this.context.route("**/*", async (route, request) => {
|
|
461
|
+
if (cookieGuard) {
|
|
462
|
+
try {
|
|
463
|
+
await this.guardCookies("request", request.url());
|
|
464
|
+
} catch {
|
|
465
|
+
await route.abort("failed").catch(() => undefined);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
435
469
|
let mainDocument = false;
|
|
436
470
|
if (request.isNavigationRequest() && !request.redirectedFrom()) {
|
|
437
471
|
try {
|
|
@@ -848,24 +882,31 @@ export class Browser {
|
|
|
848
882
|
`captureRegionByScroll: rect must have a positive size (got ${rect.width}×${rect.height})`,
|
|
849
883
|
);
|
|
850
884
|
}
|
|
851
|
-
const settle = (top: number) =>
|
|
852
|
-
page.evaluate(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
last =
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
885
|
+
const settle = (top: number, left?: number) =>
|
|
886
|
+
page.evaluate(
|
|
887
|
+
async (target: { top: number; left?: number }) => {
|
|
888
|
+
window.scrollTo({
|
|
889
|
+
left: target.left ?? window.scrollX,
|
|
890
|
+
top: target.top,
|
|
891
|
+
behavior: "instant",
|
|
892
|
+
});
|
|
893
|
+
const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
|
|
894
|
+
let last = Number.NaN;
|
|
895
|
+
for (let i = 0; i < 8; i++) {
|
|
896
|
+
await frame();
|
|
897
|
+
const now = window.scrollY;
|
|
898
|
+
if (now === last) break;
|
|
899
|
+
last = now;
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
scrollX: window.scrollX,
|
|
903
|
+
scrollY: window.scrollY,
|
|
904
|
+
innerWidth: window.innerWidth,
|
|
905
|
+
innerHeight: window.innerHeight,
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
{ top, left },
|
|
909
|
+
);
|
|
869
910
|
const original = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
870
911
|
const captures: Buffer[] = [];
|
|
871
912
|
// Viewport-pinned candidates, measured at scroll 0 so each later capture
|
|
@@ -884,6 +925,8 @@ export class Browser {
|
|
|
884
925
|
priorOpacityPriority: string;
|
|
885
926
|
priorTransition: string;
|
|
886
927
|
priorTransitionPriority: string;
|
|
928
|
+
priorStyle: string | null;
|
|
929
|
+
appliedStyle?: string;
|
|
887
930
|
}> = [];
|
|
888
931
|
// Fixed controls can live inside a static shadow host. Walk open roots
|
|
889
932
|
// without hiding their ordinary content or exceeding the total node cap.
|
|
@@ -907,6 +950,7 @@ export class Browser {
|
|
|
907
950
|
priorOpacityPriority: el.style.getPropertyPriority("opacity"),
|
|
908
951
|
priorTransition: el.style.getPropertyValue("transition"),
|
|
909
952
|
priorTransitionPriority: el.style.getPropertyPriority("transition"),
|
|
953
|
+
priorStyle: el.getAttribute("style"),
|
|
910
954
|
});
|
|
911
955
|
}
|
|
912
956
|
return out;
|
|
@@ -916,16 +960,36 @@ export class Browser {
|
|
|
916
960
|
async ([entries, action]) => {
|
|
917
961
|
for (const entry of entries) {
|
|
918
962
|
const restore = () => {
|
|
963
|
+
if (entry.appliedStyle === undefined) return;
|
|
964
|
+
const unchanged = entry.el.getAttribute("style") === entry.appliedStyle;
|
|
919
965
|
// Restore opacity while transitions are still disabled. Restoring both
|
|
920
966
|
// declarations in one style flush starts a new fade from zero.
|
|
921
|
-
|
|
922
|
-
|
|
967
|
+
if (
|
|
968
|
+
entry.el.style.getPropertyValue("opacity") === "0" &&
|
|
969
|
+
entry.el.style.getPropertyPriority("opacity") === "important"
|
|
970
|
+
)
|
|
971
|
+
entry.el.style.setProperty(
|
|
972
|
+
"opacity",
|
|
973
|
+
entry.priorOpacity,
|
|
974
|
+
entry.priorOpacityPriority,
|
|
975
|
+
);
|
|
923
976
|
void getComputedStyle(entry.el).opacity;
|
|
924
|
-
|
|
925
|
-
"transition"
|
|
926
|
-
entry.
|
|
927
|
-
|
|
928
|
-
|
|
977
|
+
if (
|
|
978
|
+
entry.el.style.getPropertyValue("transition") === "none" &&
|
|
979
|
+
entry.el.style.getPropertyPriority("transition") === "important"
|
|
980
|
+
)
|
|
981
|
+
entry.el.style.setProperty(
|
|
982
|
+
"transition",
|
|
983
|
+
entry.priorTransition,
|
|
984
|
+
entry.priorTransitionPriority,
|
|
985
|
+
);
|
|
986
|
+
// Preserve the exact original attribute, including its absence. Do
|
|
987
|
+
// not replace styles changed by the page while our override was active.
|
|
988
|
+
if (unchanged) {
|
|
989
|
+
if (entry.priorStyle === null) entry.el.removeAttribute("style");
|
|
990
|
+
else entry.el.setAttribute("style", entry.priorStyle);
|
|
991
|
+
}
|
|
992
|
+
entry.appliedStyle = undefined;
|
|
929
993
|
};
|
|
930
994
|
if (action === "restore") {
|
|
931
995
|
restore();
|
|
@@ -933,8 +997,21 @@ export class Browser {
|
|
|
933
997
|
}
|
|
934
998
|
const now = entry.el.getBoundingClientRect().top + window.scrollY;
|
|
935
999
|
if (Math.abs(now - entry.top) > 1) {
|
|
1000
|
+
if (entry.appliedStyle !== undefined) {
|
|
1001
|
+
if (entry.el.getAttribute("style") !== entry.appliedStyle)
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
"Pinned styles changed during native capture; source is unstable.",
|
|
1004
|
+
);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
entry.priorStyle = entry.el.getAttribute("style");
|
|
1008
|
+
entry.priorOpacity = entry.el.style.getPropertyValue("opacity");
|
|
1009
|
+
entry.priorOpacityPriority = entry.el.style.getPropertyPriority("opacity");
|
|
1010
|
+
entry.priorTransition = entry.el.style.getPropertyValue("transition");
|
|
1011
|
+
entry.priorTransitionPriority = entry.el.style.getPropertyPriority("transition");
|
|
936
1012
|
entry.el.style.setProperty("transition", "none", "important");
|
|
937
1013
|
entry.el.style.setProperty("opacity", "0", "important");
|
|
1014
|
+
entry.appliedStyle = entry.el.getAttribute("style")!;
|
|
938
1015
|
} else {
|
|
939
1016
|
restore();
|
|
940
1017
|
}
|
|
@@ -990,11 +1067,8 @@ export class Browser {
|
|
|
990
1067
|
await syncPinned("restore");
|
|
991
1068
|
} finally {
|
|
992
1069
|
await pinned.dispose();
|
|
993
|
-
await
|
|
994
|
-
|
|
995
|
-
window.scrollTo({ left: x, top: y, behavior: "instant" }),
|
|
996
|
-
original,
|
|
997
|
-
);
|
|
1070
|
+
await settle(original.y, original.x);
|
|
1071
|
+
await this.waitForReviewReady();
|
|
998
1072
|
}
|
|
999
1073
|
}
|
|
1000
1074
|
return captures;
|
|
@@ -1863,10 +1937,17 @@ export class Browser {
|
|
|
1863
1937
|
* visual line. Word-count per line via per-word Range rects (the width
|
|
1864
1938
|
* of the last line is deliberately NOT the signal; see runts.ts).
|
|
1865
1939
|
*/
|
|
1866
|
-
async checkRunts(
|
|
1940
|
+
async checkRunts(
|
|
1941
|
+
opts: { scope?: string | null; minChars?: number; maxFindings?: number } = {},
|
|
1942
|
+
): Promise<RuntsResult> {
|
|
1943
|
+
const maxFindings = opts.maxFindings === undefined ? 50 : opts.maxFindings;
|
|
1944
|
+
if (!Number.isInteger(maxFindings) || maxFindings < 1 || maxFindings > 5000) {
|
|
1945
|
+
throw new Error("maxFindings must be an integer from 1 to 5000.");
|
|
1946
|
+
}
|
|
1867
1947
|
return await this.currentPage.evaluate(buildRuntsCheck(), {
|
|
1868
1948
|
scope: opts.scope ?? null,
|
|
1869
1949
|
minChars: opts.minChars ?? 40,
|
|
1950
|
+
maxFindings,
|
|
1870
1951
|
});
|
|
1871
1952
|
}
|
|
1872
1953
|
|
|
@@ -1973,6 +2054,30 @@ export class Browser {
|
|
|
1973
2054
|
await this.context.addInitScript(script);
|
|
1974
2055
|
}
|
|
1975
2056
|
|
|
2057
|
+
/** The error a `cookieGuard` threw, once it has stopped the session. */
|
|
2058
|
+
get cookieGuardFault(): unknown {
|
|
2059
|
+
return this.cookieFault;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private cookieFault: unknown;
|
|
2063
|
+
|
|
2064
|
+
/** Run the cookie guard; on a throw, record it, tear the context down, and rethrow. */
|
|
2065
|
+
private async guardCookies(phase: CookieGuardPhase, url: string | null): Promise<void> {
|
|
2066
|
+
const guard = this.opts.cookieGuard;
|
|
2067
|
+
const context = this.context;
|
|
2068
|
+
if (!guard || !context || this.cookieFault) {
|
|
2069
|
+
if (this.cookieFault) throw this.cookieFault;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
await guard(await context.cookies(), { phase, url });
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
this.cookieFault ??= error;
|
|
2076
|
+
await this.close();
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1976
2081
|
/** Snapshot all cookies in the live persistent context. */
|
|
1977
2082
|
async cookies(): Promise<PWCookie[]> {
|
|
1978
2083
|
if (!this.context) throw new Error("Browser not opened. Call open() first.");
|
|
@@ -1985,7 +2090,14 @@ export class Browser {
|
|
|
1985
2090
|
*/
|
|
1986
2091
|
async close(): Promise<void> {
|
|
1987
2092
|
if (!this.context) return;
|
|
1988
|
-
if (this.opts.
|
|
2093
|
+
if (this.opts.cookieGuard && !this.cookieFault) {
|
|
2094
|
+
try {
|
|
2095
|
+
await this.guardCookies("close", null);
|
|
2096
|
+
} catch {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (this.opts.jar && !this.cookieFault) {
|
|
1989
2101
|
try {
|
|
1990
2102
|
const live = await this.context.cookies();
|
|
1991
2103
|
this.opts.jar.merge(live.map(toJarCookie));
|
|
@@ -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
|
}
|