harnery 0.18.0 → 0.19.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/README.md +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +187 -4
- package/dist/commands/supervisor.js +36 -1
- package/dist/core/supervisor/index.d.ts +2 -2
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +2 -2
- package/dist/core/supervisor/plan-read.d.ts +2 -1
- package/dist/core/supervisor/plan-read.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.js +175 -2
- package/dist/core/supervisor/plan-types.d.ts +48 -1
- package/dist/core/supervisor/plan-types.d.ts.map +1 -1
- package/dist/core/supervisor/plan-types.js +2 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -1
- package/dist/core/supervisor/planning.js +705 -50
- package/dist/core/supervisor/read.d.ts +2 -2
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +2 -2
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +74 -6
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +24 -6
- package/dist/core/workflow/types.d.ts +6 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +3 -2
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +31 -3
- 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 +39 -0
- package/dist/lib/browser/geometry.d.ts +146 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -0
- package/dist/lib/browser/geometry.js +525 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/target-size.d.ts +65 -0
- package/dist/lib/browser/target-size.d.ts.map +1 -0
- package/dist/lib/browser/target-size.js +108 -0
- package/package.json +7 -1
- package/src/commands/browse.ts +319 -0
- package/src/commands/supervisor.ts +45 -1
- package/src/core/supervisor/index.ts +14 -1
- package/src/core/supervisor/plan-read.ts +234 -1
- package/src/core/supervisor/plan-types.ts +55 -0
- package/src/core/supervisor/planning.ts +896 -57
- package/src/core/supervisor/read.ts +14 -1
- package/src/core/supervisor/state.ts +96 -3
- package/src/core/workflow/engine.ts +29 -6
- package/src/core/workflow/types.ts +6 -0
- package/src/core/workflow/validate.ts +34 -3
- package/src/lib/browser/client.ts +62 -0
- package/src/lib/browser/geometry.ts +695 -0
- package/src/lib/browser/index.ts +25 -0
- 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
|
|
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(
|
|
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 =
|
|
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
|
-
//
|
|
682
|
-
//
|
|
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
|
-
`${
|
|
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
|
|
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(
|
|
@@ -206,6 +206,12 @@ export interface StageSchema {
|
|
|
206
206
|
required?: string[];
|
|
207
207
|
/** type=array */
|
|
208
208
|
items?: StageSchema;
|
|
209
|
+
minItems?: number;
|
|
210
|
+
maxItems?: number;
|
|
211
|
+
/** type=string */
|
|
212
|
+
minLength?: number;
|
|
213
|
+
maxLength?: number;
|
|
214
|
+
pattern?: string;
|
|
209
215
|
/** any type: closed value set (compared with ===) */
|
|
210
216
|
enum?: Array<string | number | boolean>;
|
|
211
217
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
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
4
|
* validation depth workflow gates don't need. Supported: type, properties,
|
|
5
|
-
* required, items,
|
|
6
|
-
*
|
|
5
|
+
* required, items, minItems/maxItems, minLength/maxLength, pattern, and enum.
|
|
6
|
+
* Returns a list of human-readable problems (empty = valid) so the engine can
|
|
7
|
+
* feed failures back into the retry prompt verbatim.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { StageSchema } from "./types.ts";
|
|
@@ -34,6 +35,12 @@ export function validateAgainstSchema(value: unknown, schema: StageSchema, path
|
|
|
34
35
|
}
|
|
35
36
|
case "array": {
|
|
36
37
|
if (!Array.isArray(value)) return [`${path}: expected array, got ${short(value)}`];
|
|
38
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
39
|
+
problems.push(`${path}: expected at least ${schema.minItems} item(s), got ${value.length}`);
|
|
40
|
+
}
|
|
41
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
42
|
+
problems.push(`${path}: expected at most ${schema.maxItems} item(s), got ${value.length}`);
|
|
43
|
+
}
|
|
37
44
|
if (schema.items) {
|
|
38
45
|
for (const [i, item] of value.entries()) {
|
|
39
46
|
problems.push(
|
|
@@ -43,7 +50,31 @@ export function validateAgainstSchema(value: unknown, schema: StageSchema, path
|
|
|
43
50
|
}
|
|
44
51
|
return problems;
|
|
45
52
|
}
|
|
46
|
-
case "string":
|
|
53
|
+
case "string": {
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
return [`${path}: expected string, got ${short(value)}`];
|
|
56
|
+
}
|
|
57
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
58
|
+
problems.push(
|
|
59
|
+
`${path}: expected at least ${schema.minLength} character(s), got ${value.length}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
63
|
+
problems.push(
|
|
64
|
+
`${path}: expected at most ${schema.maxLength} character(s), got ${value.length}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (schema.pattern !== undefined) {
|
|
68
|
+
try {
|
|
69
|
+
if (!new RegExp(schema.pattern).test(value)) {
|
|
70
|
+
problems.push(`${path}: expected string matching ${JSON.stringify(schema.pattern)}`);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
problems.push(`${path}: schema pattern is invalid`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return problems;
|
|
77
|
+
}
|
|
47
78
|
case "number":
|
|
48
79
|
case "boolean": {
|
|
49
80
|
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;
|