harnery 0.37.0 → 0.38.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 +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Judge a page review pack: one bounded pool of vision calls over every tile
|
|
2
|
+
// of every context, read from disk, with no browser open. This is the second
|
|
3
|
+
// half of the capture-then-judge split; `page-review-pack.ts` owns the first.
|
|
4
|
+
//
|
|
5
|
+
// Reuse: when a persisted QA snapshot exists for a context, the band-diff
|
|
6
|
+
// planner compares the pack's own full-page screenshot with the baseline and
|
|
7
|
+
// skips tiles whose pixels provably did not change and carried no baseline
|
|
8
|
+
// finding (false reviews are allowed, false passes never).
|
|
9
|
+
//
|
|
10
|
+
// Toolkit tier: this module must not import src/core (layering check).
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import type { CritiqueFinding, CritiqueProvider, CritiqueTile } from "./critique.js";
|
|
14
|
+
import {
|
|
15
|
+
listPackContexts,
|
|
16
|
+
type PageReviewContextRecord,
|
|
17
|
+
type PageReviewCritiqueRecord,
|
|
18
|
+
readPackContext,
|
|
19
|
+
readPackFullPage,
|
|
20
|
+
readPackTiles,
|
|
21
|
+
} from "./page-review-pack.js";
|
|
22
|
+
import type { QaContext } from "./qa-plan.js";
|
|
23
|
+
import { type CritiqueReusePlan, planCritiqueReuse } from "./qa-reuse.js";
|
|
24
|
+
import { loadQaSnapshot, type QaSnapshotStoreOptions } from "./qa-snapshot.js";
|
|
25
|
+
|
|
26
|
+
export interface JudgePageReviewPackOptions {
|
|
27
|
+
packDir: string;
|
|
28
|
+
/** Host-injected vision call. Absent: every context reports `skipped`. */
|
|
29
|
+
provider: CritiqueProvider | undefined;
|
|
30
|
+
rubric: string;
|
|
31
|
+
/** Vision calls in flight across the whole pack. Default: the provider's
|
|
32
|
+
* own concurrency, then 4. */
|
|
33
|
+
concurrency?: number;
|
|
34
|
+
/** Subset of context ids to judge (default: every context in the pack). */
|
|
35
|
+
contextIds?: string[];
|
|
36
|
+
/** Band-diff reuse against persisted snapshots keyed by `target`. */
|
|
37
|
+
reuse?: {
|
|
38
|
+
target: string;
|
|
39
|
+
store?: QaSnapshotStoreOptions;
|
|
40
|
+
mismatchThreshold?: number;
|
|
41
|
+
};
|
|
42
|
+
/** Absolute epoch-ms deadline. Workers stop claiming tiles once past it;
|
|
43
|
+
* a context with unjudged tiles reports `incomplete`, never `pass`. */
|
|
44
|
+
deadlineAt?: number;
|
|
45
|
+
onLog?: (message: string) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface JudgedContext extends PageReviewCritiqueRecord {
|
|
49
|
+
record: PageReviewContextRecord;
|
|
50
|
+
/** Tiles selected for review that the pool never reached (deadline). */
|
|
51
|
+
tiles_unjudged: number;
|
|
52
|
+
reuse?: Pick<
|
|
53
|
+
CritiqueReusePlan,
|
|
54
|
+
"tiles_total" | "tiles_reused" | "tiles_reviewed" | "invalidation" | "mismatch_threshold"
|
|
55
|
+
>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface JudgePageReviewPackResult {
|
|
59
|
+
contexts: JudgedContext[];
|
|
60
|
+
pool: { concurrency: number; wall_time_ms: number; provider: string };
|
|
61
|
+
tiles_total: number;
|
|
62
|
+
tiles_reviewed: number;
|
|
63
|
+
tiles_reused: number;
|
|
64
|
+
outcome: "pass" | "fail" | "skipped" | "incomplete";
|
|
65
|
+
provider_meta?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface WorkItem {
|
|
69
|
+
contextIndex: number;
|
|
70
|
+
tile: CritiqueTile & { id: string };
|
|
71
|
+
url: string;
|
|
72
|
+
/** The rubric with this tile's position preamble in front of it. */
|
|
73
|
+
rubric: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Orientation the provider reads before the rubric: which band this tile is,
|
|
78
|
+
* where it sits on the page, how much of it repeats a neighbour, whether it
|
|
79
|
+
* touches the page top or bottom, and how it was captured. A tile edge that
|
|
80
|
+
* continues in the next band is not a clipped element, and a fixed header at
|
|
81
|
+
* the top of a scrolled-band capture is not a layout defect; the preamble
|
|
82
|
+
* says so where the pixels cannot. Adds context only: the finding taxonomy
|
|
83
|
+
* the rubric defines is unchanged. Empty for an unknown tile id.
|
|
84
|
+
*/
|
|
85
|
+
export function tileRubricPreamble(record: PageReviewContextRecord, tileId: string): string {
|
|
86
|
+
const tile = record.tiles.find((entry) => entry.id === tileId);
|
|
87
|
+
if (!tile) return "";
|
|
88
|
+
const pageHeight = record.page.height;
|
|
89
|
+
const top = tile.scrollY;
|
|
90
|
+
const bottom = tile.scrollY + tile.height;
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
if (tile.scope !== undefined) {
|
|
93
|
+
lines.push(
|
|
94
|
+
`Tile ${tile.id}: selector tile for ${tile.scope} (${tile.label}), ` +
|
|
95
|
+
`document y ${top}-${bottom} px of a ${pageHeight} px tall page.`,
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
const bands = record.tiles
|
|
99
|
+
.filter((entry) => entry.scope === undefined)
|
|
100
|
+
.sort((a, b) => a.scrollY - b.scrollY || a.index - b.index);
|
|
101
|
+
const n = bands.findIndex((entry) => entry.id === tile.id);
|
|
102
|
+
const capped = record.coverage.capped
|
|
103
|
+
? ` (the page has ${record.coverage.bands_total} bands; those past the cap are not in this pack)`
|
|
104
|
+
: "";
|
|
105
|
+
lines.push(
|
|
106
|
+
`Tile ${tile.id}: band ${n + 1} of ${bands.length}${capped}, ` +
|
|
107
|
+
`document y ${top}-${bottom} px of a ${pageHeight} px tall page.`,
|
|
108
|
+
);
|
|
109
|
+
const prev = bands[n - 1];
|
|
110
|
+
const next = bands[n + 1];
|
|
111
|
+
const overlaps: string[] = [];
|
|
112
|
+
if (prev) {
|
|
113
|
+
const above = Math.max(0, prev.scrollY + prev.height - top);
|
|
114
|
+
overlaps.push(
|
|
115
|
+
above > 0
|
|
116
|
+
? `its top ${above} px repeat the bottom of ${prev.id}`
|
|
117
|
+
: `it starts where ${prev.id} ends, with no overlap`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (next) {
|
|
121
|
+
const below = Math.max(0, bottom - next.scrollY);
|
|
122
|
+
overlaps.push(
|
|
123
|
+
below > 0
|
|
124
|
+
? `its bottom ${below} px repeat the top of ${next.id}`
|
|
125
|
+
: `it ends where ${next.id} starts, with no overlap`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (overlaps.length > 0) {
|
|
129
|
+
const joined = overlaps.join("; ");
|
|
130
|
+
lines.push(`${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const touchesTop = top <= 0;
|
|
134
|
+
const touchesBottom = bottom >= pageHeight;
|
|
135
|
+
if (touchesTop && touchesBottom) {
|
|
136
|
+
lines.push("This tile covers the whole page height.");
|
|
137
|
+
} else if (touchesTop) {
|
|
138
|
+
lines.push(
|
|
139
|
+
"This tile touches the page top; content cut at its bottom edge continues in the next band.",
|
|
140
|
+
);
|
|
141
|
+
} else if (touchesBottom) {
|
|
142
|
+
lines.push(
|
|
143
|
+
"This tile touches the page bottom; content cut at its top edge continues in the previous band.",
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(
|
|
147
|
+
"This tile is interior: content cut at its top and bottom edges continues in the neighbouring bands.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const source = record.capture_fidelity?.source ?? "full-page";
|
|
151
|
+
lines.push(
|
|
152
|
+
source === "scrolled-bands"
|
|
153
|
+
? "Capture source: scrolled-bands (the viewport was scrolled to this band and clipped, because " +
|
|
154
|
+
"the full-page screenshot disagreed with the viewport render). Fixed and sticky elements " +
|
|
155
|
+
"are shown once, at their document position, as a full-page screenshot shows them; a " +
|
|
156
|
+
"fixed bar that is absent from this band is therefore not a defect."
|
|
157
|
+
: "Capture source: full-page (one full-page screenshot, cropped to this band).",
|
|
158
|
+
);
|
|
159
|
+
return lines.join(" ");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function providerLabel(meta: Record<string, unknown> | undefined, present: boolean): string {
|
|
163
|
+
if (meta) {
|
|
164
|
+
for (const key of ["provider", "active_harness", "route", "model"]) {
|
|
165
|
+
const value = meta[key];
|
|
166
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
167
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return present ? "host" : "none";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Judge every tile in the pack through one pool. Findings keep the tiler
|
|
175
|
+
* index (`tile`) for compatibility with `CritiqueFinding` and add the pack's
|
|
176
|
+
* stable `tile_id`. A provider throw on one tile becomes a `high`
|
|
177
|
+
* `provider-error` finding for that tile; nothing else is affected.
|
|
178
|
+
*/
|
|
179
|
+
export async function judgePageReviewPack(
|
|
180
|
+
options: JudgePageReviewPackOptions,
|
|
181
|
+
): Promise<JudgePageReviewPackResult> {
|
|
182
|
+
const { packDir, provider, rubric } = options;
|
|
183
|
+
const log = options.onLog ?? (() => {});
|
|
184
|
+
const ids = options.contextIds ?? listPackContexts(packDir);
|
|
185
|
+
const records = ids.map((id) => readPackContext(packDir, id));
|
|
186
|
+
const started = Date.now();
|
|
187
|
+
|
|
188
|
+
const perContext: Array<{
|
|
189
|
+
record: PageReviewContextRecord;
|
|
190
|
+
tiles: Array<CritiqueTile & { id: string }>;
|
|
191
|
+
review: Array<CritiqueTile & { id: string }>;
|
|
192
|
+
reuse?: CritiqueReusePlan;
|
|
193
|
+
findings: Array<CritiqueFinding & { tile_id: string }>;
|
|
194
|
+
judged: Set<string>;
|
|
195
|
+
}> = records.map((record) => {
|
|
196
|
+
const tiles = readPackTiles(packDir, record);
|
|
197
|
+
let review = tiles;
|
|
198
|
+
let reuse: CritiqueReusePlan | undefined;
|
|
199
|
+
// Reuse only for unscoped captures: scope tiles share tiler indices with
|
|
200
|
+
// the bands, and a mixed set would mis-associate baseline findings.
|
|
201
|
+
// Never for a scrolled-bands context: its tiles were cut from scrolled
|
|
202
|
+
// viewport captures, not from the full-page PNG the band diff compares.
|
|
203
|
+
const scrolled = record.capture_fidelity?.source === "scrolled-bands";
|
|
204
|
+
if (options.reuse && provider && scrolled) {
|
|
205
|
+
log(`judge ${record.id}: band-diff reuse skipped (tiles came from scrolled bands)`);
|
|
206
|
+
}
|
|
207
|
+
if (options.reuse && provider && record.scopes.length === 0 && !scrolled) {
|
|
208
|
+
const context: QaContext = {
|
|
209
|
+
viewport: record.viewport,
|
|
210
|
+
theme: record.theme,
|
|
211
|
+
state: record.state,
|
|
212
|
+
};
|
|
213
|
+
const stored = loadQaSnapshot(options.reuse.target, context, options.reuse.store ?? {});
|
|
214
|
+
if (stored?.screenshotPath && stored.critique) {
|
|
215
|
+
reuse = planCritiqueReuse({
|
|
216
|
+
baselineScreenshot: readFileSync(stored.screenshotPath),
|
|
217
|
+
baselineCritique: stored.critique,
|
|
218
|
+
currentScreenshot: readPackFullPage(packDir, record),
|
|
219
|
+
tiles,
|
|
220
|
+
rubric,
|
|
221
|
+
...(options.reuse.mismatchThreshold !== undefined
|
|
222
|
+
? { mismatchThreshold: options.reuse.mismatchThreshold }
|
|
223
|
+
: {}),
|
|
224
|
+
});
|
|
225
|
+
const keep = new Set(reuse.review.map((tile) => tile.index));
|
|
226
|
+
// planCritiqueReuse returns CritiqueTile objects; re-attach ids by
|
|
227
|
+
// tiler index so the pool cites the pack's stable tile ids.
|
|
228
|
+
review = tiles.filter((tile) => keep.has(tile.index));
|
|
229
|
+
log(
|
|
230
|
+
`judge ${record.id}: ${reuse.tiles_reused}/${reuse.tiles_total} tiles reused (band-diff)` +
|
|
231
|
+
(reuse.invalidation ? ` — ${reuse.invalidation}` : ""),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { record, tiles, review, ...(reuse ? { reuse } : {}), findings: [], judged: new Set() };
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const work: WorkItem[] = [];
|
|
239
|
+
perContext.forEach((entry, contextIndex) => {
|
|
240
|
+
for (const tile of entry.review) {
|
|
241
|
+
const preamble = tileRubricPreamble(entry.record, tile.id);
|
|
242
|
+
work.push({
|
|
243
|
+
contextIndex,
|
|
244
|
+
tile,
|
|
245
|
+
url: entry.record.url,
|
|
246
|
+
rubric: preamble ? `${preamble}\n\n${rubric}` : rubric,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
let meta: Record<string, unknown> | undefined;
|
|
252
|
+
let concurrency = 0;
|
|
253
|
+
if (provider && work.length > 0) {
|
|
254
|
+
concurrency = Math.max(
|
|
255
|
+
1,
|
|
256
|
+
Math.min(options.concurrency ?? provider.concurrency ?? 4, work.length),
|
|
257
|
+
);
|
|
258
|
+
log(`judge: ${work.length} tile(s) across ${records.length} context(s), pool ${concurrency}`);
|
|
259
|
+
let cursor = 0;
|
|
260
|
+
const worker = async (): Promise<void> => {
|
|
261
|
+
while (true) {
|
|
262
|
+
if (options.deadlineAt !== undefined && Date.now() >= options.deadlineAt) return;
|
|
263
|
+
const i = cursor++;
|
|
264
|
+
if (i >= work.length) return;
|
|
265
|
+
const item = work[i] as WorkItem;
|
|
266
|
+
const entry = perContext[item.contextIndex];
|
|
267
|
+
if (!entry) continue;
|
|
268
|
+
entry.judged.add(item.tile.id);
|
|
269
|
+
try {
|
|
270
|
+
const found = await provider({ url: item.url, rubric: item.rubric, tile: item.tile });
|
|
271
|
+
for (const finding of found) {
|
|
272
|
+
entry.findings.push({ ...finding, tile: item.tile.index, tile_id: item.tile.id });
|
|
273
|
+
}
|
|
274
|
+
} catch (err: unknown) {
|
|
275
|
+
entry.findings.push({
|
|
276
|
+
tile: item.tile.index,
|
|
277
|
+
tile_id: item.tile.id,
|
|
278
|
+
severity: "high",
|
|
279
|
+
category: "provider-error",
|
|
280
|
+
description: `critique provider failed on ${item.tile.label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
286
|
+
meta = provider.meta?.();
|
|
287
|
+
} else if (provider) {
|
|
288
|
+
meta = provider.meta?.();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const label = providerLabel(meta, Boolean(provider));
|
|
292
|
+
const contexts: JudgedContext[] = perContext.map((entry) => {
|
|
293
|
+
// Deterministic order: by tile id, then insertion.
|
|
294
|
+
const findings = [...entry.findings].sort((a, b) => a.tile_id.localeCompare(b.tile_id));
|
|
295
|
+
const unjudged = entry.review.filter((tile) => !entry.judged.has(tile.id)).length;
|
|
296
|
+
const outcome: JudgedContext["outcome"] = !provider
|
|
297
|
+
? "skipped"
|
|
298
|
+
: findings.some((f) => f.severity === "high")
|
|
299
|
+
? "fail"
|
|
300
|
+
: unjudged > 0
|
|
301
|
+
? "incomplete"
|
|
302
|
+
: "pass";
|
|
303
|
+
return {
|
|
304
|
+
context_id: entry.record.id,
|
|
305
|
+
provider: provider ? label : "none",
|
|
306
|
+
tiles_total: entry.tiles.length,
|
|
307
|
+
tiles_reviewed: entry.review.length - unjudged,
|
|
308
|
+
tiles_unjudged: unjudged,
|
|
309
|
+
tiles_reused: entry.reuse?.tiles_reused ?? 0,
|
|
310
|
+
outcome,
|
|
311
|
+
findings,
|
|
312
|
+
coverage: entry.record.coverage,
|
|
313
|
+
...(provider
|
|
314
|
+
? {}
|
|
315
|
+
: {
|
|
316
|
+
error:
|
|
317
|
+
"no critiqueProvider injected by the host (see HarneryProgramContext.critiqueProvider)",
|
|
318
|
+
}),
|
|
319
|
+
record: entry.record,
|
|
320
|
+
...(entry.reuse
|
|
321
|
+
? {
|
|
322
|
+
reuse: {
|
|
323
|
+
tiles_total: entry.reuse.tiles_total,
|
|
324
|
+
tiles_reused: entry.reuse.tiles_reused,
|
|
325
|
+
tiles_reviewed: entry.reuse.tiles_reviewed,
|
|
326
|
+
mismatch_threshold: entry.reuse.mismatch_threshold,
|
|
327
|
+
...(entry.reuse.invalidation ? { invalidation: entry.reuse.invalidation } : {}),
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const tilesTotal = contexts.reduce((sum, c) => sum + c.tiles_total, 0);
|
|
335
|
+
const tilesReviewed = contexts.reduce((sum, c) => sum + c.tiles_reviewed, 0);
|
|
336
|
+
const tilesReused = contexts.reduce((sum, c) => sum + c.tiles_reused, 0);
|
|
337
|
+
const outcome: JudgePageReviewPackResult["outcome"] = !provider
|
|
338
|
+
? "skipped"
|
|
339
|
+
: contexts.some((c) => c.outcome === "fail")
|
|
340
|
+
? "fail"
|
|
341
|
+
: contexts.some((c) => c.outcome === "incomplete")
|
|
342
|
+
? "incomplete"
|
|
343
|
+
: "pass";
|
|
344
|
+
return {
|
|
345
|
+
contexts,
|
|
346
|
+
pool: { concurrency, wall_time_ms: Date.now() - started, provider: label },
|
|
347
|
+
tiles_total: tilesTotal,
|
|
348
|
+
tiles_reviewed: tilesReviewed,
|
|
349
|
+
tiles_reused: tilesReused,
|
|
350
|
+
outcome,
|
|
351
|
+
...(meta ? { provider_meta: meta } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Strip the in-memory `record` so the result can be written into the pack. */
|
|
356
|
+
export function toCritiqueRecords(result: JudgePageReviewPackResult): PageReviewCritiqueRecord[] {
|
|
357
|
+
return result.contexts.map(
|
|
358
|
+
({ record: _record, reuse: _reuse, tiles_unjudged: _unjudged, ...rest }) => rest,
|
|
359
|
+
);
|
|
360
|
+
}
|