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
|
@@ -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})`
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// Toolkit tier: this module must not import src/core (layering check).
|
|
19
19
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
|
+
import type { CritiqueUsage, CritiqueUsageTotals } from "./critique.js";
|
|
22
|
+
import { PAGE_REVIEW_MAX_TILE_BUDGET } from "./page-review-contracts.js";
|
|
21
23
|
import type { QaContext, QaManifest } from "./qa-plan.js";
|
|
22
24
|
|
|
23
25
|
export const QA_RUN_JOB_SCHEMA_VERSION = 1 as const;
|
|
@@ -152,6 +154,9 @@ export interface QaRunCritiqueTileTiming {
|
|
|
152
154
|
tile_id: string;
|
|
153
155
|
duration_ms: number;
|
|
154
156
|
outcome: "passed" | "failed";
|
|
157
|
+
/** Tokens the provider reported for this tile, retries included. Absent
|
|
158
|
+
* when the backend reports none; that is unknown, not zero. */
|
|
159
|
+
usage?: CritiqueUsage;
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
/** What share of the page the critique tiles covered, lifted from the browse
|
|
@@ -184,6 +189,11 @@ export interface QaRunCritiqueOutcome {
|
|
|
184
189
|
dismissed?: Array<{ finding_key: string; target: string; disposition: string }>;
|
|
185
190
|
/** Tile coverage of the page. Absent when the capture died or reported none. */
|
|
186
191
|
coverage?: QaRunCritiqueCoverage;
|
|
192
|
+
/** `full`: every band reviewed. `capped`: bands left out; a signoff blocker.
|
|
193
|
+
* `sampled`: signoff on a page whose complete review would exceed the
|
|
194
|
+
* per-run tile ceiling; every edge band, gate hit, and required scope was
|
|
195
|
+
* reviewed and the uncovered intervals are listed in `coverage` (ADR 0224). */
|
|
196
|
+
coverage_mode?: "full" | "capped" | "sampled";
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
/** The judge stage as one unit: every context's tiles through one bounded
|
|
@@ -199,6 +209,8 @@ export interface QaRunCritiquePool {
|
|
|
199
209
|
latency_ms?: Record<string, QaRunCritiqueLatency>;
|
|
200
210
|
/** One entry per attempted tile, in context and tile order. */
|
|
201
211
|
tile_timings?: QaRunCritiqueTileTiming[];
|
|
212
|
+
/** Tokens summed over the attempted tiles. */
|
|
213
|
+
usage?: CritiqueUsageTotals;
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
/** Where the run's page review pack lives: the on-disk evidence an agent can
|
|
@@ -518,8 +530,15 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
|
|
|
518
530
|
}
|
|
519
531
|
if (p?.critique_tile_budget !== undefined) {
|
|
520
532
|
const n = p.critique_tile_budget;
|
|
521
|
-
if (
|
|
522
|
-
|
|
533
|
+
if (
|
|
534
|
+
typeof n !== "number" ||
|
|
535
|
+
!Number.isInteger(n) ||
|
|
536
|
+
n < 8 ||
|
|
537
|
+
n > PAGE_REVIEW_MAX_TILE_BUDGET
|
|
538
|
+
) {
|
|
539
|
+
errors.push(
|
|
540
|
+
`policy.critique_tile_budget must be an integer between 8 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
|
|
541
|
+
);
|
|
523
542
|
}
|
|
524
543
|
}
|
|
525
544
|
if (p?.command_concurrency !== undefined) {
|
|
@@ -542,8 +561,15 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
|
|
|
542
561
|
}
|
|
543
562
|
if (p?.critique_max_tiles !== undefined) {
|
|
544
563
|
const n = p.critique_max_tiles;
|
|
545
|
-
if (
|
|
546
|
-
|
|
564
|
+
if (
|
|
565
|
+
typeof n !== "number" ||
|
|
566
|
+
!Number.isInteger(n) ||
|
|
567
|
+
n < 1 ||
|
|
568
|
+
n > PAGE_REVIEW_MAX_TILE_BUDGET
|
|
569
|
+
) {
|
|
570
|
+
errors.push(
|
|
571
|
+
`policy.critique_max_tiles must be an integer between 1 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
|
|
572
|
+
);
|
|
547
573
|
}
|
|
548
574
|
}
|
|
549
575
|
if (p?.critique_pool !== undefined) {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./page-review-budget.js";
|
|
25
25
|
import {
|
|
26
26
|
PAGE_REVIEW_DEFAULT_TILE_BUDGET,
|
|
27
|
+
PAGE_REVIEW_MAX_TILE_BUDGET,
|
|
27
28
|
type PageReviewCapturePlan,
|
|
28
29
|
type PageReviewContextAllocation,
|
|
29
30
|
} from "./page-review-contracts.js";
|
|
@@ -911,10 +912,29 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
911
912
|
log(`critique provider failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
912
913
|
}
|
|
913
914
|
}
|
|
914
|
-
const bandArgs =
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
915
|
+
const bandArgs = (ctx: QaRunContext): string[] => {
|
|
916
|
+
const args = [
|
|
917
|
+
...contextRenderArgs(ctx),
|
|
918
|
+
...(job.checks ?? [])
|
|
919
|
+
.filter((check) => check.contexts === undefined || check.contexts.includes(ctx.id))
|
|
920
|
+
.flatMap((check) => check.args),
|
|
921
|
+
];
|
|
922
|
+
let requested = "1400";
|
|
923
|
+
for (let i = 0; i < args.length; i++) {
|
|
924
|
+
if (args[i] === "--check-critique-band") requested = args[i + 1] ?? "";
|
|
925
|
+
else if (args[i]?.startsWith("--check-critique-band=")) {
|
|
926
|
+
requested = args[i]!.slice("--check-critique-band=".length);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// A larger explicit request needs a declared route budget. Missing or
|
|
930
|
+
// invalid metadata must not turn capture planning into an unbounded path.
|
|
931
|
+
const budget = provider?.tileBudgetPx;
|
|
932
|
+
const cap = budget !== undefined && Number.isFinite(budget) && budget >= 200 ? budget : 1400;
|
|
933
|
+
return [
|
|
934
|
+
"--check-critique-band",
|
|
935
|
+
String(Math.max(200, Math.min(Number.parseInt(requested, 10), cap))),
|
|
936
|
+
];
|
|
937
|
+
};
|
|
918
938
|
const scopeArgs =
|
|
919
939
|
visual === "full-page"
|
|
920
940
|
? []
|
|
@@ -961,7 +981,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
961
981
|
"--qa-state",
|
|
962
982
|
ctx.state,
|
|
963
983
|
...scopeArgs,
|
|
964
|
-
...bandArgs,
|
|
984
|
+
...bandArgs(ctx),
|
|
965
985
|
...critiqueMaxTilesArgs,
|
|
966
986
|
]
|
|
967
987
|
: []),
|
|
@@ -1185,6 +1205,10 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1185
1205
|
|
|
1186
1206
|
enterStage("capture");
|
|
1187
1207
|
const captureStart = Date.now();
|
|
1208
|
+
// Candidate tiles a complete review of every context would need. Above the
|
|
1209
|
+
// per-run ceiling, signoff accepts a sampled critique (ADR 0224).
|
|
1210
|
+
let fullCoverageTiles = 0;
|
|
1211
|
+
const sampledContexts = new Set<string>();
|
|
1188
1212
|
if (runsVisual) {
|
|
1189
1213
|
writeFileSync(
|
|
1190
1214
|
join(outDir, "preliminary-gates.json"),
|
|
@@ -1205,6 +1229,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1205
1229
|
);
|
|
1206
1230
|
return candidate;
|
|
1207
1231
|
});
|
|
1232
|
+
fullCoverageTiles = plans.reduce((sum, plan) => sum + plan.candidates.length, 0);
|
|
1208
1233
|
const ceilings =
|
|
1209
1234
|
job.policy?.critique_max_tiles === undefined
|
|
1210
1235
|
? undefined
|
|
@@ -1263,7 +1288,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1263
1288
|
.flatMap((check) => check.args),
|
|
1264
1289
|
...scopeArgs,
|
|
1265
1290
|
...critiqueMaxTilesArgs,
|
|
1266
|
-
...bandArgs,
|
|
1291
|
+
...bandArgs(ctx),
|
|
1267
1292
|
];
|
|
1268
1293
|
log(
|
|
1269
1294
|
`capture ${ctx.id}${scopeArgs.length > 0 ? ` [${manifest.scopes.map((s) => s.selector).join(",")}]` : ""}`,
|
|
@@ -1398,6 +1423,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1398
1423
|
provider: judged.pool.provider,
|
|
1399
1424
|
...(latency ? { latency_ms: latency } : {}),
|
|
1400
1425
|
tile_timings: judged.tile_timings,
|
|
1426
|
+
...(judged.usage ? { usage: judged.usage } : {}),
|
|
1401
1427
|
};
|
|
1402
1428
|
for (const row of judged.contexts) {
|
|
1403
1429
|
judgedById.set(row.context_id, row);
|
|
@@ -1477,17 +1503,43 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1477
1503
|
});
|
|
1478
1504
|
}
|
|
1479
1505
|
// Selected bands may leave gaps anywhere. Signoff needs complete
|
|
1480
|
-
// coverage
|
|
1506
|
+
// coverage when the whole page fits under the per-run ceiling. Past
|
|
1507
|
+
// it, complete coverage is unavailable by policy, so signoff accepts a
|
|
1508
|
+
// sampled critique that still reviewed every edge band, gate hit, and
|
|
1509
|
+
// required scope; the deterministic gates covered the whole page.
|
|
1510
|
+
let coverageMode: QaRunCritiqueOutcome["coverage_mode"] = row.coverage
|
|
1511
|
+
? row.coverage.capped
|
|
1512
|
+
? "capped"
|
|
1513
|
+
: "full"
|
|
1514
|
+
: undefined;
|
|
1481
1515
|
if (job.mode === "signoff" && row.coverage.capped && contextOutcome !== "failed") {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1516
|
+
// Without the allocation record nothing proves the required regions
|
|
1517
|
+
// were selected, so a sampled signoff is refused.
|
|
1518
|
+
const allocated = row.record.allocation_coverage;
|
|
1519
|
+
const requiredOmitted = allocated
|
|
1520
|
+
? [
|
|
1521
|
+
...allocated.omitted_gate_hits.map((id) => `gate hit ${id}`),
|
|
1522
|
+
...allocated.omitted_scopes.map((scope) => `scope ${scope}`),
|
|
1523
|
+
]
|
|
1524
|
+
: ["its allocation record (recapture this context)"];
|
|
1525
|
+
if (fullCoverageTiles > PAGE_REVIEW_MAX_TILE_BUDGET && requiredOmitted.length === 0) {
|
|
1526
|
+
coverageMode = "sampled";
|
|
1527
|
+
sampledContexts.add(row.context_id);
|
|
1528
|
+
} else {
|
|
1529
|
+
contextOutcome = "unknown";
|
|
1530
|
+
blockers.push({
|
|
1531
|
+
stage: "critique",
|
|
1532
|
+
context_id: row.context_id,
|
|
1533
|
+
reason:
|
|
1534
|
+
`critique coverage capped: ${row.coverage.bands_reviewed} of ${row.coverage.bands_total} bands ` +
|
|
1535
|
+
`reviewed (${row.coverage.reviewed_height_px} of ${row.coverage.page_height_px} px)` +
|
|
1536
|
+
(requiredOmitted.length > 0
|
|
1537
|
+
? `; the budget left out ${requiredOmitted.join(", ")}. Raise ` +
|
|
1538
|
+
`policy.critique_tile_budget (at most ${PAGE_REVIEW_MAX_TILE_BUDGET}) so every gate hit and scope is reviewed`
|
|
1539
|
+
: `; the full page needs ${fullCoverageTiles} tiles, so raise ` +
|
|
1540
|
+
`policy.critique_tile_budget (and any explicit per-context ceiling) to ${fullCoverageTiles}`),
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1491
1543
|
}
|
|
1492
1544
|
const scopeById = new Map(row.record.tiles.map((tile) => [tile.id, tile.scope]));
|
|
1493
1545
|
critique.push({
|
|
@@ -1497,6 +1549,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1497
1549
|
tiles_reviewed: row.tiles_reviewed,
|
|
1498
1550
|
tiles_reused: row.tiles_reused,
|
|
1499
1551
|
outcome: contextOutcome,
|
|
1552
|
+
...(coverageMode ? { coverage_mode: coverageMode } : {}),
|
|
1500
1553
|
...(dismissed.length > 0 ? { dismissed } : {}),
|
|
1501
1554
|
findings: row.findings.map((finding) => {
|
|
1502
1555
|
const selector = scopeById.get(finding.tile_id);
|
|
@@ -1510,7 +1563,15 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1510
1563
|
coverage: row.coverage,
|
|
1511
1564
|
});
|
|
1512
1565
|
}
|
|
1513
|
-
finalizePack(
|
|
1566
|
+
finalizePack(
|
|
1567
|
+
"critique",
|
|
1568
|
+
toCritiqueRecords(judged).map((record) =>
|
|
1569
|
+
sampledContexts.has(record.context_id)
|
|
1570
|
+
? { ...record, coverage_mode: "sampled" as const }
|
|
1571
|
+
: record,
|
|
1572
|
+
),
|
|
1573
|
+
judged.pool,
|
|
1574
|
+
);
|
|
1514
1575
|
}
|
|
1515
1576
|
if (runsVisual) restoreHeadlessOnly();
|
|
1516
1577
|
wall.critique = Date.now() - critiqueStart;
|