harnery 0.18.0 → 0.19.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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/browse.d.ts.map +1 -1
  3. package/dist/commands/browse.js +187 -4
  4. package/dist/commands/supervisor.js +36 -1
  5. package/dist/core/supervisor/index.d.ts +2 -2
  6. package/dist/core/supervisor/index.d.ts.map +1 -1
  7. package/dist/core/supervisor/index.js +2 -2
  8. package/dist/core/supervisor/plan-read.d.ts +2 -1
  9. package/dist/core/supervisor/plan-read.d.ts.map +1 -1
  10. package/dist/core/supervisor/plan-read.js +175 -2
  11. package/dist/core/supervisor/plan-types.d.ts +48 -1
  12. package/dist/core/supervisor/plan-types.d.ts.map +1 -1
  13. package/dist/core/supervisor/plan-types.js +2 -0
  14. package/dist/core/supervisor/planning.d.ts.map +1 -1
  15. package/dist/core/supervisor/planning.js +762 -60
  16. package/dist/core/supervisor/read.d.ts +2 -2
  17. package/dist/core/supervisor/read.d.ts.map +1 -1
  18. package/dist/core/supervisor/read.js +2 -2
  19. package/dist/core/supervisor/state.d.ts.map +1 -1
  20. package/dist/core/supervisor/state.js +74 -6
  21. package/dist/core/workflow/engine.d.ts.map +1 -1
  22. package/dist/core/workflow/engine.js +24 -6
  23. package/dist/core/workflow/types.d.ts +9 -0
  24. package/dist/core/workflow/types.d.ts.map +1 -1
  25. package/dist/core/workflow/validate.d.ts +5 -3
  26. package/dist/core/workflow/validate.d.ts.map +1 -1
  27. package/dist/core/workflow/validate.js +53 -4
  28. package/dist/lib/browser/client.d.ts +18 -0
  29. package/dist/lib/browser/client.d.ts.map +1 -1
  30. package/dist/lib/browser/client.js +39 -0
  31. package/dist/lib/browser/geometry.d.ts +146 -0
  32. package/dist/lib/browser/geometry.d.ts.map +1 -0
  33. package/dist/lib/browser/geometry.js +525 -0
  34. package/dist/lib/browser/index.d.ts +2 -0
  35. package/dist/lib/browser/index.d.ts.map +1 -1
  36. package/dist/lib/browser/target-size.d.ts +65 -0
  37. package/dist/lib/browser/target-size.d.ts.map +1 -0
  38. package/dist/lib/browser/target-size.js +108 -0
  39. package/package.json +7 -1
  40. package/src/commands/browse.ts +319 -0
  41. package/src/commands/supervisor.ts +45 -1
  42. package/src/core/supervisor/index.ts +14 -1
  43. package/src/core/supervisor/plan-read.ts +234 -1
  44. package/src/core/supervisor/plan-types.ts +55 -0
  45. package/src/core/supervisor/planning.ts +954 -65
  46. package/src/core/supervisor/read.ts +14 -1
  47. package/src/core/supervisor/state.ts +96 -3
  48. package/src/core/workflow/engine.ts +29 -6
  49. package/src/core/workflow/types.ts +9 -0
  50. package/src/core/workflow/validate.ts +59 -4
  51. package/src/lib/browser/client.ts +62 -0
  52. package/src/lib/browser/geometry.ts +695 -0
  53. package/src/lib/browser/index.ts +25 -0
  54. package/src/lib/browser/target-size.ts +180 -0
@@ -1,5 +1,11 @@
1
- export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
2
1
  export {
2
+ readSupervisorPlan,
3
+ readSupervisorPlanReviewReceipt,
4
+ readSupervisorPlans,
5
+ } from "./plan-read.ts";
6
+ export {
7
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
8
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
3
9
  SUPERVISOR_PLAN_SCHEMA_VERSION,
4
10
  type SupervisorPlanEvent,
5
11
  type SupervisorPlanEventType,
@@ -9,6 +15,13 @@ export {
9
15
  type SupervisorPlanProposal,
10
16
  type SupervisorPlanRecord,
11
17
  type SupervisorPlanRequest,
18
+ type SupervisorPlanReviewFinding,
19
+ type SupervisorPlanReviewReceipt,
20
+ type SupervisorPlanReviewReviewer,
21
+ type SupervisorPlanReviewRound,
22
+ type SupervisorPlanReviewStatus,
23
+ type SupervisorPlanReviewSummary,
24
+ type SupervisorPlanReviewVerdict,
12
25
  type SupervisorPlanStatus,
13
26
  type SupervisorPlanTemplate,
14
27
  type SupervisorPlanWorkSpec,
@@ -20,6 +20,8 @@ import type { WorkflowSpecialistProfile } from "../workflow/types.ts";
20
20
  import { readSupervisorPlans } from "./plan-read.ts";
21
21
  import {
22
22
  type CreateSupervisorReplanningInput,
23
+ MAX_SUPERVISOR_PLAN_REVIEWERS,
24
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
23
25
  type SupervisorPlanHistory,
24
26
  type SupervisorPlanRecord,
25
27
  type SupervisorReplanningPolicy,
@@ -157,8 +159,9 @@ export function createSupervisor(input: CreateSupervisorInput): SupervisorRecord
157
159
  }
158
160
  if (input.rootWorkId) assertWorkId(input.rootWorkId);
159
161
  const root = input.rootWorkId ? readWorkItem(coordRoot, input.rootWorkId) : undefined;
162
+ const limits = normalizeLimits(input.limits);
160
163
  const replanning = input.replanning
161
- ? normalizeReplanning(input.replanning, input.specialists)
164
+ ? normalizeReplanning(input.replanning, input.specialists, limits)
162
165
  : undefined;
163
166
  if (
164
167
  mission &&
@@ -180,7 +183,7 @@ export function createSupervisor(input: CreateSupervisorInput): SupervisorRecord
180
183
  ),
181
184
  root_work_id: input.rootWorkId ?? missionRootId(id),
182
185
  specialists: normalizeWorkflowSpecialists(input.specialists),
183
- limits: normalizeLimits(input.limits),
186
+ limits,
184
187
  automation: normalizeAutomation(input.automation),
185
188
  ...(mission ? { mission } : {}),
186
189
  ...(replanning ? { replanning } : {}),
@@ -655,7 +658,7 @@ function validateIntent(intent: SupervisorIntent, goalId: string): void {
655
658
  ) {
656
659
  throw new Error(`supervisor intent ${goalId} automation policy is not canonical`);
657
660
  }
658
- if (intent.replanning) validateReplanning(intent.replanning, specialists, goalId);
661
+ if (intent.replanning) validateReplanning(intent.replanning, specialists, intent.limits, goalId);
659
662
  }
660
663
 
661
664
  function normalizeMission(input: CreateSupervisorMissionInput): SupervisorMission {
@@ -720,6 +723,7 @@ function normalizeAutomation(
720
723
  function normalizeReplanning(
721
724
  input: CreateSupervisorReplanningInput,
722
725
  specialistsInput: Readonly<Record<string, WorkflowSpecialistProfile>>,
726
+ limits: SupervisorLimits,
723
727
  ): SupervisorReplanningPolicy {
724
728
  const specialists = normalizeWorkflowSpecialists(specialistsInput);
725
729
  const plannerSpecialist = bounded(input.plannerSpecialist, "planner specialist", 100);
@@ -769,6 +773,9 @@ function normalizeReplanning(
769
773
  if (maxWorkItemsPerPlan > maxTotalWorkItems) {
770
774
  throw new Error("replanning max_work_items_per_plan cannot exceed max_total_work_items");
771
775
  }
776
+ const review = input.review
777
+ ? normalizeReviewPolicy(input.review, specialists, plannerSpecialist, limits)
778
+ : undefined;
772
779
  return {
773
780
  planner_specialist: plannerSpecialist,
774
781
  auto_apply: boolean(input.autoApply ?? false, "replanning auto_apply"),
@@ -776,12 +783,60 @@ function normalizeReplanning(
776
783
  max_work_items_per_plan: maxWorkItemsPerPlan,
777
784
  max_total_work_items: maxTotalWorkItems,
778
785
  templates,
786
+ ...(review ? { review } : {}),
787
+ };
788
+ }
789
+
790
+ function normalizeReviewPolicy(
791
+ input: NonNullable<CreateSupervisorReplanningInput["review"]>,
792
+ specialists: Record<string, WorkflowSpecialistProfile>,
793
+ plannerSpecialist: string,
794
+ limits: SupervisorLimits,
795
+ ): NonNullable<SupervisorReplanningPolicy["review"]> {
796
+ if (!Array.isArray(input.reviewerSpecialists) || input.reviewerSpecialists.length < 1) {
797
+ throw new Error("replanning review requires at least one reviewer specialist");
798
+ }
799
+ if (input.reviewerSpecialists.length > MAX_SUPERVISOR_PLAN_REVIEWERS) {
800
+ throw new Error(
801
+ `replanning review cannot exceed ${MAX_SUPERVISOR_PLAN_REVIEWERS} reviewer specialists`,
802
+ );
803
+ }
804
+ const reviewerSpecialists = input.reviewerSpecialists.map((specialist, index) =>
805
+ bounded(specialist, `reviewer specialist[${index}]`, 100),
806
+ );
807
+ if (new Set(reviewerSpecialists).size !== reviewerSpecialists.length) {
808
+ throw new Error("replanning reviewer specialists must be unique");
809
+ }
810
+ for (const specialist of reviewerSpecialists) {
811
+ if (!specialists[specialist]) {
812
+ throw new Error(`reviewer specialist ${specialist} is not present in the frozen team`);
813
+ }
814
+ if (specialist === plannerSpecialist) {
815
+ throw new Error("reviewer specialist cannot be the planner specialist");
816
+ }
817
+ }
818
+ const maxRevisionRounds = nonNegative(
819
+ input.maxRevisionRounds,
820
+ "replanning review max_revision_rounds",
821
+ MAX_SUPERVISOR_PLAN_REVISION_ROUNDS,
822
+ );
823
+ const worstCaseAgents =
824
+ 1 + reviewerSpecialists.length * (maxRevisionRounds + 1) + maxRevisionRounds;
825
+ if (worstCaseAgents > limits.max_agents_per_work) {
826
+ throw new Error(
827
+ `replanning review worst-case ${worstCaseAgents} agents exceeds max_agents_per_work ${limits.max_agents_per_work}`,
828
+ );
829
+ }
830
+ return {
831
+ reviewer_specialists: reviewerSpecialists,
832
+ max_revision_rounds: maxRevisionRounds,
779
833
  };
780
834
  }
781
835
 
782
836
  function validateReplanning(
783
837
  policy: SupervisorReplanningPolicy,
784
838
  specialists: Record<string, WorkflowSpecialistProfile>,
839
+ limits: SupervisorLimits,
785
840
  goalId: string,
786
841
  ): void {
787
842
  if (!specialists[policy.planner_specialist]) {
@@ -814,6 +869,37 @@ function validateReplanning(
814
869
  rootCapable ||= template.root;
815
870
  }
816
871
  if (!rootCapable) throw new Error(`supervisor ${goalId} has no root-capable replanning template`);
872
+ if (policy.review !== undefined) {
873
+ const review = policy.review;
874
+ if (
875
+ !Array.isArray(review.reviewer_specialists) ||
876
+ review.reviewer_specialists.length < 1 ||
877
+ review.reviewer_specialists.length > MAX_SUPERVISOR_PLAN_REVIEWERS ||
878
+ !Number.isSafeInteger(review.max_revision_rounds) ||
879
+ review.max_revision_rounds < 0 ||
880
+ review.max_revision_rounds > MAX_SUPERVISOR_PLAN_REVISION_ROUNDS
881
+ ) {
882
+ throw new Error(`supervisor ${goalId} replanning review policy is invalid`);
883
+ }
884
+ if (new Set(review.reviewer_specialists).size !== review.reviewer_specialists.length) {
885
+ throw new Error(`supervisor ${goalId} replanning reviewers are not unique`);
886
+ }
887
+ for (const specialist of review.reviewer_specialists) {
888
+ if (!specialists[specialist]) {
889
+ throw new Error(`supervisor ${goalId} reviewer specialist is not in the frozen team`);
890
+ }
891
+ if (specialist === policy.planner_specialist) {
892
+ throw new Error(`supervisor ${goalId} reviewer specialist cannot be planner`);
893
+ }
894
+ }
895
+ const worstCaseAgents =
896
+ 1 +
897
+ review.reviewer_specialists.length * (review.max_revision_rounds + 1) +
898
+ review.max_revision_rounds;
899
+ if (worstCaseAgents > limits.max_agents_per_work) {
900
+ throw new Error(`supervisor ${goalId} review policy exceeds the agent budget`);
901
+ }
902
+ }
817
903
  }
818
904
 
819
905
  function collectGovernedWork(
@@ -915,6 +1001,13 @@ function positive(value: unknown, field: string, max: number): number {
915
1001
  return value as number;
916
1002
  }
917
1003
 
1004
+ function nonNegative(value: unknown, field: string, max: number): number {
1005
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > max) {
1006
+ throw new Error(`${field} must be an integer between 0 and ${max}`);
1007
+ }
1008
+ return value as number;
1009
+ }
1010
+
918
1011
  function boolean(value: unknown, field: string): boolean {
919
1012
  if (typeof value !== "boolean") throw new Error(`supervisor ${field} must be boolean`);
920
1013
  return value;
@@ -58,6 +58,7 @@ import type {
58
58
  HarnessName,
59
59
  RunReport,
60
60
  SpawnResult,
61
+ StageSchema,
61
62
  WorkflowAgentProof,
62
63
  WorkflowContext,
63
64
  WorkflowEvidenceRecord,
@@ -423,6 +424,9 @@ async function executeWorkflow(
423
424
  const assignment = resolveSpecialistAssignment(specialists, prompt, requestedOpts);
424
425
  const agentOpts = assignment.opts;
425
426
  const assignmentPrompt = assignment.prompt;
427
+ const dispatchPrompt = agentOpts.schema
428
+ ? withSchemaContract(assignmentPrompt, agentOpts.schema)
429
+ : assignmentPrompt;
426
430
  let reservedForDispatch = 0;
427
431
  let spawnCountClaimed = false;
428
432
  const harness = agentOpts.harness ?? defaultHarness;
@@ -487,7 +491,7 @@ async function executeWorkflow(
487
491
  max_attempts: maxAttempts,
488
492
  max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
489
493
  timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
490
- prompt_bytes: Buffer.byteLength(assignmentPrompt),
494
+ prompt_bytes: Buffer.byteLength(dispatchPrompt),
491
495
  isolation,
492
496
  network_access: networkAccess,
493
497
  current_cost_usd: round4(costUsd + reservedCostUsd),
@@ -609,7 +613,7 @@ async function executeWorkflow(
609
613
  });
610
614
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${harness}] ${label}`);
611
615
 
612
- let attemptPrompt = assignmentPrompt;
616
+ let attemptPrompt = dispatchPrompt;
613
617
  let last: SpawnResult | null = null;
614
618
  let agentCostUsd = 0;
615
619
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -678,12 +682,16 @@ async function executeWorkflow(
678
682
  }
679
683
 
680
684
  journal("agent.schema_retry", { id, attempt, problems });
681
- // Feed the validation failure back verbatim — the retry prompt carries
682
- // exactly what was wrong, which is what makes bounded retry converge.
685
+ // Keep the original cache identity while making the actual output
686
+ // contract explicit. A full bounded copy of the rejected value lets the
687
+ // child repair omissions instead of reconstructing an unseen object.
683
688
  attemptPrompt =
684
- `${assignmentPrompt}\n\nYour previous reply failed validation:\n` +
689
+ `${dispatchPrompt}\n\nYour previous reply failed validation. ` +
690
+ `Return a complete replacement value, not a patch.\n\n` +
691
+ `Previous reply (JSON string): ${JSON.stringify(boundedSchemaReply(last.text))}\n\n` +
692
+ `Validation problems:\n` +
685
693
  `${problems.map((p) => ` - ${p}`).join("\n")}\n` +
686
- `Reply with ONLY the corrected JSON object. No prose, no code fences.`;
694
+ `Reply with ONLY the corrected JSON value. No prose, no code fences.`;
687
695
  }
688
696
 
689
697
  const reason = last?.ok
@@ -892,6 +900,21 @@ async function executeWorkflow(
892
900
  }
893
901
  }
894
902
 
903
+ const MAX_SCHEMA_RETRY_REPLY = 16_000;
904
+
905
+ function withSchemaContract(prompt: string, schema: StageSchema): string {
906
+ return (
907
+ `${prompt}\n\nOutput contract:\n` +
908
+ `Return ONLY one JSON value matching this schema. Do not use prose or code fences.\n` +
909
+ `${JSON.stringify(schema)}`
910
+ );
911
+ }
912
+
913
+ function boundedSchemaReply(value: string): string {
914
+ if (value.length <= MAX_SCHEMA_RETRY_REPLY) return value;
915
+ return `${value.slice(0, MAX_SCHEMA_RETRY_REPLY)}\n[truncated by Harnery]`;
916
+ }
917
+
895
918
  /** Stable identity for one agent() call, for the resume cache. The ORIGINAL
896
919
  * prompt (never a retry-mutated one) plus everything that changes behavior. */
897
920
  function agentCallKey(
@@ -201,11 +201,20 @@ export interface HarnessEvidenceCapability {
201
201
  /** JSON-schema *subset* accepted by stage gates (see validate.ts). */
202
202
  export interface StageSchema {
203
203
  type: "object" | "array" | "string" | "number" | "boolean";
204
+ /** Exact-one branch selection, evaluated before the base schema. */
205
+ oneOf?: StageSchema[];
204
206
  /** type=object */
205
207
  properties?: Record<string, StageSchema>;
206
208
  required?: string[];
209
+ additionalProperties?: boolean;
207
210
  /** type=array */
208
211
  items?: StageSchema;
212
+ minItems?: number;
213
+ maxItems?: number;
214
+ /** type=string */
215
+ minLength?: number;
216
+ maxLength?: number;
217
+ pattern?: string;
209
218
  /** any type: closed value set (compared with ===) */
210
219
  enum?: Array<string | number | boolean>;
211
220
  }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Minimal validator for the StageSchema JSON-schema subset. Deliberately tiny:
3
3
  * a full JSON Schema implementation would pull a dependency (ajv) for
4
- * validation depth workflow gates don't need. Supported: type, properties,
5
- * required, items, enum. Returns a list of human-readable problems (empty =
6
- * valid) so the engine can feed failures back into the retry prompt verbatim.
4
+ * validation depth workflow gates don't need. Supported: type, oneOf,
5
+ * properties, required, additionalProperties, items, minItems/maxItems,
6
+ * minLength/maxLength, pattern, and enum.
7
+ * Returns a list of human-readable problems (empty = valid) so the engine can
8
+ * feed failures back into the retry prompt verbatim.
7
9
  */
8
10
 
9
11
  import type { StageSchema } from "./types.ts";
@@ -11,6 +13,23 @@ import type { StageSchema } from "./types.ts";
11
13
  export function validateAgainstSchema(value: unknown, schema: StageSchema, path = "$"): string[] {
12
14
  const problems: string[] = [];
13
15
 
16
+ if (schema.oneOf) {
17
+ const branches = schema.oneOf.map((branch) => validateAgainstSchema(value, branch, path));
18
+ const matches = branches.filter((branch) => branch.length === 0).length;
19
+ if (matches !== 1) {
20
+ if (matches > 1) {
21
+ return [`${path}: expected exactly one schema option to match, got ${matches}`];
22
+ }
23
+ return [
24
+ `${path}: expected exactly one schema option to match`,
25
+ ...branches.map(
26
+ (branch, index) =>
27
+ `${path}: option ${index + 1}: ${branch.slice(0, 4).join("; ") || "did not match"}`,
28
+ ),
29
+ ];
30
+ }
31
+ }
32
+
14
33
  if (schema.enum) {
15
34
  if (!schema.enum.some((v) => v === value)) {
16
35
  problems.push(`${path}: expected one of ${JSON.stringify(schema.enum)}, got ${short(value)}`);
@@ -30,10 +49,22 @@ export function validateAgainstSchema(value: unknown, schema: StageSchema, path
30
49
  for (const [key, sub] of Object.entries(schema.properties ?? {})) {
31
50
  if (key in obj) problems.push(...validateAgainstSchema(obj[key], sub, `${path}.${key}`));
32
51
  }
52
+ if (schema.additionalProperties === false) {
53
+ const allowed = new Set(Object.keys(schema.properties ?? {}));
54
+ for (const key of Object.keys(obj)) {
55
+ if (!allowed.has(key)) problems.push(`${path}.${key}: unexpected property`);
56
+ }
57
+ }
33
58
  return problems;
34
59
  }
35
60
  case "array": {
36
61
  if (!Array.isArray(value)) return [`${path}: expected array, got ${short(value)}`];
62
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
63
+ problems.push(`${path}: expected at least ${schema.minItems} item(s), got ${value.length}`);
64
+ }
65
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
66
+ problems.push(`${path}: expected at most ${schema.maxItems} item(s), got ${value.length}`);
67
+ }
37
68
  if (schema.items) {
38
69
  for (const [i, item] of value.entries()) {
39
70
  problems.push(
@@ -43,7 +74,31 @@ export function validateAgainstSchema(value: unknown, schema: StageSchema, path
43
74
  }
44
75
  return problems;
45
76
  }
46
- case "string":
77
+ case "string": {
78
+ if (typeof value !== "string") {
79
+ return [`${path}: expected string, got ${short(value)}`];
80
+ }
81
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
82
+ problems.push(
83
+ `${path}: expected at least ${schema.minLength} character(s), got ${value.length}`,
84
+ );
85
+ }
86
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) {
87
+ problems.push(
88
+ `${path}: expected at most ${schema.maxLength} character(s), got ${value.length}`,
89
+ );
90
+ }
91
+ if (schema.pattern !== undefined) {
92
+ try {
93
+ if (!new RegExp(schema.pattern).test(value)) {
94
+ problems.push(`${path}: expected string matching ${JSON.stringify(schema.pattern)}`);
95
+ }
96
+ } catch {
97
+ problems.push(`${path}: schema pattern is invalid`);
98
+ }
99
+ }
100
+ return problems;
101
+ }
47
102
  case "number":
48
103
  case "boolean": {
49
104
  if (typeof value !== schema.type) {
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
+ import axe from "axe-core";
4
5
  import {
5
6
  type BrowserContext,
6
7
  type ConsoleMessage,
@@ -10,6 +11,13 @@ import {
10
11
  type Request,
11
12
  } from "playwright";
12
13
  import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
14
+ import {
15
+ buildClearLayoutLintAnnotationsScript,
16
+ buildLayoutLintAnnotateScript,
17
+ buildLayoutLintCheck,
18
+ type LayoutLintRequest,
19
+ type LayoutLintResult,
20
+ } from "./geometry.js";
13
21
  import {
14
22
  buildClearLayoutAnnotationsScript,
15
23
  buildLayoutAnnotateScript,
@@ -24,6 +32,13 @@ import {
24
32
  buildRuntsCheck,
25
33
  type RuntsResult,
26
34
  } from "./runts.js";
35
+ import {
36
+ buildClearTargetSizeAnnotationsScript,
37
+ buildTargetSizeAnnotateScript,
38
+ buildTargetSizeCheck,
39
+ type TargetSizeProfile,
40
+ type TargetSizeResult,
41
+ } from "./target-size.js";
27
42
  import {
28
43
  buildAnnotateScript,
29
44
  buildClearAnnotationsScript,
@@ -441,6 +456,53 @@ export class Browser {
441
456
  await this.currentPage.evaluate(buildClearLayoutAnnotationsScript());
442
457
  }
443
458
 
459
+ /** Run the selector-scoped rendered-geometry rule family in one page evaluation. */
460
+ async checkLayoutLint(request: LayoutLintRequest): Promise<LayoutLintResult> {
461
+ return await this.currentPage.evaluate(buildLayoutLintCheck(), request);
462
+ }
463
+
464
+ /** Draw one document-space annotation layer for rendered-geometry results. */
465
+ async annotateLayoutLint(result: LayoutLintResult): Promise<void> {
466
+ await this.currentPage.evaluate(buildLayoutLintAnnotateScript(), result);
467
+ }
468
+
469
+ /** Remove rendered-geometry annotations. */
470
+ async clearLayoutLintAnnotations(): Promise<void> {
471
+ await this.currentPage.evaluate(buildClearLayoutLintAnnotationsScript());
472
+ }
473
+
474
+ /**
475
+ * Run the target-size rule against the page or explicit scopes. The
476
+ * dependency's browser bundle is evaluated directly in the page context so
477
+ * a document CSP cannot silently block a script element.
478
+ */
479
+ async checkTargetSize(
480
+ selectors: Array<string | null>,
481
+ profile: TargetSizeProfile,
482
+ ): Promise<TargetSizeResult[]> {
483
+ await this.currentPage.evaluate(axe.source);
484
+ const results: TargetSizeResult[] = [];
485
+ for (const selector of selectors) {
486
+ results.push(
487
+ await this.currentPage.evaluate(buildTargetSizeCheck(), {
488
+ selector,
489
+ profile,
490
+ }),
491
+ );
492
+ }
493
+ return results;
494
+ }
495
+
496
+ /** Draw target-size failures and unknowns on the screenshot. */
497
+ async annotateTargetSize(results: TargetSizeResult[]): Promise<void> {
498
+ await this.currentPage.evaluate(buildTargetSizeAnnotateScript(), results);
499
+ }
500
+
501
+ /** Remove target-size annotations. */
502
+ async clearTargetSizeAnnotations(): Promise<void> {
503
+ await this.currentPage.evaluate(buildClearTargetSizeAnnotationsScript());
504
+ }
505
+
444
506
  /**
445
507
  * Inject a script that runs in every page context before page scripts
446
508
  * execute. Useful for seeding localStorage before an SSR/CSR comparison;