@webability/core 1.8.0 → 1.10.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/NOTICES.md +0 -16
- package/README.md +1 -1
- package/dist/dom.cjs +37 -31
- package/dist/dom.d.cts +5 -4
- package/dist/dom.d.ts +5 -4
- package/dist/dom.js +37 -31
- package/dist/engine.cjs +414 -0
- package/dist/engine.d.cts +588 -0
- package/dist/engine.d.ts +588 -0
- package/dist/engine.js +414 -0
- package/dist/index.cjs +47 -45
- package/dist/index.d.cts +59 -3
- package/dist/index.d.ts +59 -3
- package/dist/index.js +47 -45
- package/dist/playwright.cjs +37 -35
- package/dist/playwright.d.cts +1 -1
- package/dist/playwright.d.ts +1 -1
- package/dist/playwright.js +37 -35
- package/dist/{types-BgiJ7PGs.d.cts → types-fjs6XhzQ.d.cts} +94 -6
- package/dist/{types-BgiJ7PGs.d.ts → types-fjs6XhzQ.d.ts} +94 -6
- package/package.json +17 -3
|
@@ -113,6 +113,12 @@ interface JudgeOptions {
|
|
|
113
113
|
apiUrl?: string;
|
|
114
114
|
/** Bearer token for the hosted path. */
|
|
115
115
|
apiToken?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Hosted path only: the shared secret that identifies the hosted MCP server
|
|
118
|
+
* to the platform (sent as `x-mcp-internal`). Lets the platform judge scans
|
|
119
|
+
* that carry no user token. Never set on local installs.
|
|
120
|
+
*/
|
|
121
|
+
internalSecret?: string;
|
|
116
122
|
/** Total judge budget per scan (all batches). Default 20 000 ms. */
|
|
117
123
|
timeoutMs?: number;
|
|
118
124
|
/** Conformance ceiling: only criteria at or below this level are promoted. Default AA. */
|
|
@@ -121,6 +127,11 @@ interface JudgeOptions {
|
|
|
121
127
|
vision?: boolean;
|
|
122
128
|
/** Test / custom model injection. Wins over apiKey/apiUrl. */
|
|
123
129
|
transport?: JudgeTransport;
|
|
130
|
+
/**
|
|
131
|
+
* Largest element crop (base64 chars) to send. Default: JUDGE_MAX_CROP_CHARS on
|
|
132
|
+
* the hosted path (the endpoint answers 413 above it, PNG only), none elsewhere.
|
|
133
|
+
*/
|
|
134
|
+
maxCropChars?: number;
|
|
124
135
|
}
|
|
125
136
|
interface JudgeRunOptions extends JudgeOptions {
|
|
126
137
|
url: string;
|
|
@@ -134,6 +145,32 @@ interface JudgeOutcome {
|
|
|
134
145
|
judged: JudgedEntry$1[];
|
|
135
146
|
}
|
|
136
147
|
declare function resetJudgeCache(): void;
|
|
148
|
+
/** The judge's system prompt. Exported so a server-side judge (platform /api/judge-incomplete) sends the same rules, not a copy. */
|
|
149
|
+
declare const JUDGE_SYSTEM_PROMPT = "You are the final accessibility judge for a WCAG scanner. Automated engines could not decide the findings below. You MUST decide each one: \"pass\" (not a violation) or \"violation\". There is no third option \u2014 never answer \"unsure\", never defer to a human.\n\nRules:\n- Images (1.1.1): \"pass\" when the image is decorative, purely presentational, a spacer/tracking pixel, or is named by the text of its enclosing link/button. Otherwise \"violation\" with fix.attribute=\"alt\" and a concrete alt text written from the image and its context: \u2264125 characters, no \"image of\"/\"picture of\" prefix, no file names.\n- CSS background images (1.1.1) on a control with no accessible name (context.control.accessibleName is empty): the image is the control's only content, so \"violation\" with fix.value = a short name for what the control does, written from the crop and nearby text (\u2264125 characters, no \"image of\"). \"pass\" only if the control is clearly not meant to be operated.\n- Images of text (1.4.5): logos, wordmarks and brand signatures are exempt \u2192 \"pass\". Essential informational text rendered as an image \u2192 \"violation\".\n- Text contrast (1.4.3 / 1.4.6): you receive the text colour, the required ratio for its font size, and the best/worst measured ratio behind the glyphs plus a crop. \"violation\" when the text is not readable at the required ratio over a meaningful part of its background; then fix.attribute=\"style\" and fix.value = a 6-digit hex text colour that meets the ratio against the background's dominant colour. \"pass\" when the text clearly meets the ratio everywhere the glyphs sit.\n- Link purpose (2.4.4): the context is the link text plus its sentence, paragraph, list item or table cell (nearbyText). \"pass\" when the text names the destination page or topic (Home, About, Info, Contact, Pricing, a product or article name), even as a single word in a site menu. \"violation\" only for generic text that names no destination (here, click here, more, read more, learn more, link) when that context does not supply it.\n- Dead in-page link (2.4.1): the item is an <a href=\"#fragment\"> near the top of the page whose fragment matches no id or <a name> (context.engineNote names it). WCAG 2.4.1 is about bypassing repeated blocks. \"violation\" only when the link is the page's bypass mechanism (a skip link that should jump past the header or navigation) and its target is missing, so activating it goes nowhere. Any other dead fragment link is \"pass\" for 2.4.1: an ordinary or script-driven link (tab, accordion, modal or menu trigger, back-to-top).\n- Form with no submit control (3.2.2): the item is a <form> with visible fields and no submit button, input type=submit/image or outside button. \"violation\" only when changing a field or pressing Enter causes an unexpected change of context (navigation, submission, reload) and no submit control exists to ask for it. \"pass\" for a script-driven form, a single search box, or a form whose fields change nothing until the user acts.\n- Decorative image with a title (1.1.1): the item is an <img alt=\"\"> that also has a non-empty title (context.engineNote gives it). \"violation\" when the picture conveys information the title names (a logo, chart, labelled icon, photo of a named subject), so the empty alt hides it. \"pass\" when the picture is purely decorative and the title only repeats nearby text or adds nothing. Do not write alt text for this item.\n- Non-text contrast (1.4.11), motion (2.2.2 / 2.3.3), duplicate ids, ARIA references, captions and everything else: decide from the markup, the context and the crop. For a \"violation\", give fix.attribute/fix.value only when you are certain of the exact value.\n- Be strict: \"pass\" only when confident the finding is not a real barrier.\n\nRespond with ONLY a JSON array, one object per item, same ids:\n[{\"id\": \"...\", \"verdict\": \"pass\" | \"violation\", \"reason\": \"one sentence\", \"fix\": {\"attribute\": \"...\", \"value\": \"...\"}}]\nOmit \"fix\" for passes.";
|
|
150
|
+
interface JudgeChatBody {
|
|
151
|
+
model: string;
|
|
152
|
+
temperature: number;
|
|
153
|
+
max_tokens: number;
|
|
154
|
+
reasoning?: {
|
|
155
|
+
enabled: false;
|
|
156
|
+
};
|
|
157
|
+
messages: Array<{
|
|
158
|
+
role: 'system' | 'user';
|
|
159
|
+
content: unknown;
|
|
160
|
+
}>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The chat-completions body for one judge batch: the vision model only when the
|
|
164
|
+
* batch is a vision batch AND carries a crop (crops as image_url parts), else the
|
|
165
|
+
* text model. `disableTextModelReasoning`: the default text model reasons unless
|
|
166
|
+
* told not to, and on a real 30-item batch spent all 4096 tokens reasoning (no
|
|
167
|
+
* verdicts, 48 s); set it only for endpoints that accept the `reasoning` field.
|
|
168
|
+
*/
|
|
169
|
+
declare function buildJudgeChatBody(req: JudgeRequest, cfg?: {
|
|
170
|
+
visionModel?: string;
|
|
171
|
+
textModel?: string;
|
|
172
|
+
disableTextModelReasoning?: boolean;
|
|
173
|
+
}): JudgeChatBody;
|
|
137
174
|
/** OpenAI-compatible chat completions (OpenRouter). Vision → image_url parts; text-only → text model. */
|
|
138
175
|
declare function openAiCompatibleTransport(cfg: {
|
|
139
176
|
apiKey: string;
|
|
@@ -144,10 +181,12 @@ declare function openAiCompatibleTransport(cfg: {
|
|
|
144
181
|
/**
|
|
145
182
|
* Hosted path: the platform judges on the server. Request body = JudgeRequest;
|
|
146
183
|
* response = `{ verdicts: JudgeVerdict[] }`. Any non-2xx → items stay dropped.
|
|
184
|
+
* The platform answers only a user bearer or the hosted MCP's `x-mcp-internal`.
|
|
147
185
|
*/
|
|
148
186
|
declare function hostedJudgeTransport(cfg: {
|
|
149
187
|
apiUrl: string;
|
|
150
188
|
token?: string;
|
|
189
|
+
internalSecret?: string;
|
|
151
190
|
}): JudgeTransport;
|
|
152
191
|
declare function judgeUndecided(page: PageLike | null, undecided: readonly ScanIssue[], opts: JudgeRunOptions): Promise<JudgeOutcome>;
|
|
153
192
|
|
|
@@ -176,9 +215,11 @@ interface PageLike {
|
|
|
176
215
|
keyboard?: {
|
|
177
216
|
press(key: string): Promise<void>;
|
|
178
217
|
};
|
|
218
|
+
/** The live Playwright Page behind a PageLike shim: axe scans iframes through it. */
|
|
219
|
+
axePage?(): unknown;
|
|
179
220
|
}
|
|
180
221
|
/** All fix issue types the pipeline can detect and fix. */
|
|
181
|
-
type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | '
|
|
222
|
+
type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'missing_landmark' | 'svg_missing_name' | 'aria_hidden_focusable' | 'adjacent_redundant_link' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'target_too_small' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'decorative_icon' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'duplicate_id' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'broken_anchor' | 'form_missing_submit' | 'decorative_image_with_title' | 'placeholder_link' | 'missing_pattern_title' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'missing_button_type' | 'link_consistency' | 'keyboard_trap' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'nested_landmark' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'empty_heading' | 'javascript_link' | 'broken_aria_reference' | 'main_content_hidden' | 'scrollable_not_focusable' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation';
|
|
182
223
|
/** WCAG criterion for each issue type. */
|
|
183
224
|
declare const WCAG_BY_ISSUE: Record<FixIssueType, string>;
|
|
184
225
|
/** A single accessibility issue detected on the page. */
|
|
@@ -318,6 +359,31 @@ interface JudgedEntry {
|
|
|
318
359
|
verdict: 'pass' | 'violation';
|
|
319
360
|
reason: string;
|
|
320
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* A finding or a page change the scanner's own tooling caused (invariant P).
|
|
364
|
+
* Gate metric, bar = 0: any entry means the scan measured our state, not the page.
|
|
365
|
+
* - state-not-restored: a tool left focus / focus-visible / scroll / selection /
|
|
366
|
+
* injected nodes changed (the scanner restored it)
|
|
367
|
+
* - finding-dropped: a finding sat on an injected (marked) node or on an
|
|
368
|
+
* element a tool left focused; it was not reported
|
|
369
|
+
*/
|
|
370
|
+
interface InternalBug {
|
|
371
|
+
tool: string;
|
|
372
|
+
kind: 'state-not-restored' | 'finding-dropped';
|
|
373
|
+
detail: string;
|
|
374
|
+
selector?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Diagnosis only: what the page DOM did inside this tool's window (the
|
|
377
|
+
* mutation-log slice). Pages mutate themselves, so this is never used to
|
|
378
|
+
* attribute a finding; it is there so a bug is traceable. Capped.
|
|
379
|
+
*/
|
|
380
|
+
mutations?: Array<{
|
|
381
|
+
type: string;
|
|
382
|
+
attr?: string | null;
|
|
383
|
+
target: string;
|
|
384
|
+
added: string[];
|
|
385
|
+
}>;
|
|
386
|
+
}
|
|
321
387
|
interface ScanResult {
|
|
322
388
|
url: string;
|
|
323
389
|
timestamp: string;
|
|
@@ -342,6 +408,18 @@ interface ScanResult {
|
|
|
342
408
|
* disables all contrast checks for the page. Absent = all subsystems healthy.
|
|
343
409
|
*/
|
|
344
410
|
engineWarnings?: string[];
|
|
411
|
+
/**
|
|
412
|
+
* Undecided findings dropped with no verdict: no resolver measurement, no
|
|
413
|
+
* model-free rule and no judge verdict (judge off, failed or silent). For
|
|
414
|
+
* these criteria an absent issue is NOT evidence of a pass. Absent = every
|
|
415
|
+
* undecided finding within the level got a verdict.
|
|
416
|
+
*/
|
|
417
|
+
unsettled?: {
|
|
418
|
+
count: number;
|
|
419
|
+
wcag: string[];
|
|
420
|
+
};
|
|
421
|
+
/** Invariant P: findings / page changes caused by our own tooling. Always present; `[]` = clean. */
|
|
422
|
+
internal_bugs?: InternalBug[];
|
|
345
423
|
/** Engine (core package) version that produced this result — provenance */
|
|
346
424
|
engine?: string;
|
|
347
425
|
/**
|
|
@@ -351,9 +429,10 @@ interface ScanResult {
|
|
|
351
429
|
*/
|
|
352
430
|
incomplete: ScanIssue[];
|
|
353
431
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
432
|
+
* Verdicts on findings the engines could not decide: model-free rules (a
|
|
433
|
+
* measured contrast pass, a decorative or 1x1 image, a reduced-motion rule)
|
|
434
|
+
* and, when configured, the AI judge. `pass` = cleared with a reason,
|
|
435
|
+
* `violation` = promoted into `issues`. Absent when nothing got a verdict.
|
|
357
436
|
*/
|
|
358
437
|
judged?: JudgedEntry[];
|
|
359
438
|
summary: {
|
|
@@ -374,12 +453,20 @@ declare const VIEWPORT_SIZES: Record<ViewportPreset, {
|
|
|
374
453
|
}>;
|
|
375
454
|
interface ScanOptions {
|
|
376
455
|
wcagTags?: string[];
|
|
456
|
+
/** @internal test seam: extra detectors run serially, each in its own state guard. */
|
|
457
|
+
extraSerialDetectors?: FixDetector[];
|
|
377
458
|
/**
|
|
378
459
|
* Highest WCAG conformance level to report. Default 'AA': findings whose
|
|
379
460
|
* criteria are all Level AAA are not reported anywhere (issues, incomplete,
|
|
380
461
|
* summary). 'AAA' opts in (and adds axe's wcag2aaa rules); 'A' keeps Level A only.
|
|
381
462
|
*/
|
|
382
463
|
level?: WcagLevel;
|
|
464
|
+
/**
|
|
465
|
+
* Run axe-core (default true). axe is the only engine for the checks it
|
|
466
|
+
* covers on the ACT test suite: form-field / select / combobox names and
|
|
467
|
+
* valid ARIA roles have no WebAbility detector. false = detector-only scan
|
|
468
|
+
* without those checks.
|
|
469
|
+
*/
|
|
383
470
|
includeAxe?: boolean;
|
|
384
471
|
deep?: boolean;
|
|
385
472
|
deepApiUrl?: string;
|
|
@@ -420,9 +507,10 @@ interface ScanOptions {
|
|
|
420
507
|
* AI judge for findings the engines could not decide. With a model key
|
|
421
508
|
* (local, OpenRouter-compatible) or `apiUrl` (hosted), each such finding
|
|
422
509
|
* becomes a verdict: a violation with a fix in `issues`, or a recorded pass
|
|
423
|
-
* in `judged`. Unset / empty =
|
|
510
|
+
* in `judged`. Unset / empty = no model calls: only the model-free verdicts
|
|
511
|
+
* run, and findings they cannot settle are dropped (listed in `unsettled`).
|
|
424
512
|
*/
|
|
425
513
|
judge?: JudgeOptions;
|
|
426
514
|
}
|
|
427
515
|
|
|
428
|
-
export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type
|
|
516
|
+
export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type InternalBug as I, JUDGE_DEFAULT_TIMEOUT_MS as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type SiteTheme as b, type ScanIssue as c, DEFAULT_JUDGE_BASE_URL as d, DEFAULT_JUDGE_TEXT_MODEL as e, DEFAULT_JUDGE_VISION_MODEL as f, type FixIssueType as g, type IssueImpact as h, type IssueSource as i, JUDGE_SYSTEM_PROMPT as j, type JudgeChatBody as k, type JudgeItem as l, type JudgeOptions as m, type JudgeRequest as n, type JudgeTransport as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
|
|
@@ -113,6 +113,12 @@ interface JudgeOptions {
|
|
|
113
113
|
apiUrl?: string;
|
|
114
114
|
/** Bearer token for the hosted path. */
|
|
115
115
|
apiToken?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Hosted path only: the shared secret that identifies the hosted MCP server
|
|
118
|
+
* to the platform (sent as `x-mcp-internal`). Lets the platform judge scans
|
|
119
|
+
* that carry no user token. Never set on local installs.
|
|
120
|
+
*/
|
|
121
|
+
internalSecret?: string;
|
|
116
122
|
/** Total judge budget per scan (all batches). Default 20 000 ms. */
|
|
117
123
|
timeoutMs?: number;
|
|
118
124
|
/** Conformance ceiling: only criteria at or below this level are promoted. Default AA. */
|
|
@@ -121,6 +127,11 @@ interface JudgeOptions {
|
|
|
121
127
|
vision?: boolean;
|
|
122
128
|
/** Test / custom model injection. Wins over apiKey/apiUrl. */
|
|
123
129
|
transport?: JudgeTransport;
|
|
130
|
+
/**
|
|
131
|
+
* Largest element crop (base64 chars) to send. Default: JUDGE_MAX_CROP_CHARS on
|
|
132
|
+
* the hosted path (the endpoint answers 413 above it, PNG only), none elsewhere.
|
|
133
|
+
*/
|
|
134
|
+
maxCropChars?: number;
|
|
124
135
|
}
|
|
125
136
|
interface JudgeRunOptions extends JudgeOptions {
|
|
126
137
|
url: string;
|
|
@@ -134,6 +145,32 @@ interface JudgeOutcome {
|
|
|
134
145
|
judged: JudgedEntry$1[];
|
|
135
146
|
}
|
|
136
147
|
declare function resetJudgeCache(): void;
|
|
148
|
+
/** The judge's system prompt. Exported so a server-side judge (platform /api/judge-incomplete) sends the same rules, not a copy. */
|
|
149
|
+
declare const JUDGE_SYSTEM_PROMPT = "You are the final accessibility judge for a WCAG scanner. Automated engines could not decide the findings below. You MUST decide each one: \"pass\" (not a violation) or \"violation\". There is no third option \u2014 never answer \"unsure\", never defer to a human.\n\nRules:\n- Images (1.1.1): \"pass\" when the image is decorative, purely presentational, a spacer/tracking pixel, or is named by the text of its enclosing link/button. Otherwise \"violation\" with fix.attribute=\"alt\" and a concrete alt text written from the image and its context: \u2264125 characters, no \"image of\"/\"picture of\" prefix, no file names.\n- CSS background images (1.1.1) on a control with no accessible name (context.control.accessibleName is empty): the image is the control's only content, so \"violation\" with fix.value = a short name for what the control does, written from the crop and nearby text (\u2264125 characters, no \"image of\"). \"pass\" only if the control is clearly not meant to be operated.\n- Images of text (1.4.5): logos, wordmarks and brand signatures are exempt \u2192 \"pass\". Essential informational text rendered as an image \u2192 \"violation\".\n- Text contrast (1.4.3 / 1.4.6): you receive the text colour, the required ratio for its font size, and the best/worst measured ratio behind the glyphs plus a crop. \"violation\" when the text is not readable at the required ratio over a meaningful part of its background; then fix.attribute=\"style\" and fix.value = a 6-digit hex text colour that meets the ratio against the background's dominant colour. \"pass\" when the text clearly meets the ratio everywhere the glyphs sit.\n- Link purpose (2.4.4): the context is the link text plus its sentence, paragraph, list item or table cell (nearbyText). \"pass\" when the text names the destination page or topic (Home, About, Info, Contact, Pricing, a product or article name), even as a single word in a site menu. \"violation\" only for generic text that names no destination (here, click here, more, read more, learn more, link) when that context does not supply it.\n- Dead in-page link (2.4.1): the item is an <a href=\"#fragment\"> near the top of the page whose fragment matches no id or <a name> (context.engineNote names it). WCAG 2.4.1 is about bypassing repeated blocks. \"violation\" only when the link is the page's bypass mechanism (a skip link that should jump past the header or navigation) and its target is missing, so activating it goes nowhere. Any other dead fragment link is \"pass\" for 2.4.1: an ordinary or script-driven link (tab, accordion, modal or menu trigger, back-to-top).\n- Form with no submit control (3.2.2): the item is a <form> with visible fields and no submit button, input type=submit/image or outside button. \"violation\" only when changing a field or pressing Enter causes an unexpected change of context (navigation, submission, reload) and no submit control exists to ask for it. \"pass\" for a script-driven form, a single search box, or a form whose fields change nothing until the user acts.\n- Decorative image with a title (1.1.1): the item is an <img alt=\"\"> that also has a non-empty title (context.engineNote gives it). \"violation\" when the picture conveys information the title names (a logo, chart, labelled icon, photo of a named subject), so the empty alt hides it. \"pass\" when the picture is purely decorative and the title only repeats nearby text or adds nothing. Do not write alt text for this item.\n- Non-text contrast (1.4.11), motion (2.2.2 / 2.3.3), duplicate ids, ARIA references, captions and everything else: decide from the markup, the context and the crop. For a \"violation\", give fix.attribute/fix.value only when you are certain of the exact value.\n- Be strict: \"pass\" only when confident the finding is not a real barrier.\n\nRespond with ONLY a JSON array, one object per item, same ids:\n[{\"id\": \"...\", \"verdict\": \"pass\" | \"violation\", \"reason\": \"one sentence\", \"fix\": {\"attribute\": \"...\", \"value\": \"...\"}}]\nOmit \"fix\" for passes.";
|
|
150
|
+
interface JudgeChatBody {
|
|
151
|
+
model: string;
|
|
152
|
+
temperature: number;
|
|
153
|
+
max_tokens: number;
|
|
154
|
+
reasoning?: {
|
|
155
|
+
enabled: false;
|
|
156
|
+
};
|
|
157
|
+
messages: Array<{
|
|
158
|
+
role: 'system' | 'user';
|
|
159
|
+
content: unknown;
|
|
160
|
+
}>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The chat-completions body for one judge batch: the vision model only when the
|
|
164
|
+
* batch is a vision batch AND carries a crop (crops as image_url parts), else the
|
|
165
|
+
* text model. `disableTextModelReasoning`: the default text model reasons unless
|
|
166
|
+
* told not to, and on a real 30-item batch spent all 4096 tokens reasoning (no
|
|
167
|
+
* verdicts, 48 s); set it only for endpoints that accept the `reasoning` field.
|
|
168
|
+
*/
|
|
169
|
+
declare function buildJudgeChatBody(req: JudgeRequest, cfg?: {
|
|
170
|
+
visionModel?: string;
|
|
171
|
+
textModel?: string;
|
|
172
|
+
disableTextModelReasoning?: boolean;
|
|
173
|
+
}): JudgeChatBody;
|
|
137
174
|
/** OpenAI-compatible chat completions (OpenRouter). Vision → image_url parts; text-only → text model. */
|
|
138
175
|
declare function openAiCompatibleTransport(cfg: {
|
|
139
176
|
apiKey: string;
|
|
@@ -144,10 +181,12 @@ declare function openAiCompatibleTransport(cfg: {
|
|
|
144
181
|
/**
|
|
145
182
|
* Hosted path: the platform judges on the server. Request body = JudgeRequest;
|
|
146
183
|
* response = `{ verdicts: JudgeVerdict[] }`. Any non-2xx → items stay dropped.
|
|
184
|
+
* The platform answers only a user bearer or the hosted MCP's `x-mcp-internal`.
|
|
147
185
|
*/
|
|
148
186
|
declare function hostedJudgeTransport(cfg: {
|
|
149
187
|
apiUrl: string;
|
|
150
188
|
token?: string;
|
|
189
|
+
internalSecret?: string;
|
|
151
190
|
}): JudgeTransport;
|
|
152
191
|
declare function judgeUndecided(page: PageLike | null, undecided: readonly ScanIssue[], opts: JudgeRunOptions): Promise<JudgeOutcome>;
|
|
153
192
|
|
|
@@ -176,9 +215,11 @@ interface PageLike {
|
|
|
176
215
|
keyboard?: {
|
|
177
216
|
press(key: string): Promise<void>;
|
|
178
217
|
};
|
|
218
|
+
/** The live Playwright Page behind a PageLike shim: axe scans iframes through it. */
|
|
219
|
+
axePage?(): unknown;
|
|
179
220
|
}
|
|
180
221
|
/** All fix issue types the pipeline can detect and fix. */
|
|
181
|
-
type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | '
|
|
222
|
+
type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'missing_landmark' | 'svg_missing_name' | 'aria_hidden_focusable' | 'adjacent_redundant_link' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'target_too_small' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'decorative_icon' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'duplicate_id' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'broken_anchor' | 'form_missing_submit' | 'decorative_image_with_title' | 'placeholder_link' | 'missing_pattern_title' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'missing_button_type' | 'link_consistency' | 'keyboard_trap' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'nested_landmark' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'empty_heading' | 'javascript_link' | 'broken_aria_reference' | 'main_content_hidden' | 'scrollable_not_focusable' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation';
|
|
182
223
|
/** WCAG criterion for each issue type. */
|
|
183
224
|
declare const WCAG_BY_ISSUE: Record<FixIssueType, string>;
|
|
184
225
|
/** A single accessibility issue detected on the page. */
|
|
@@ -318,6 +359,31 @@ interface JudgedEntry {
|
|
|
318
359
|
verdict: 'pass' | 'violation';
|
|
319
360
|
reason: string;
|
|
320
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* A finding or a page change the scanner's own tooling caused (invariant P).
|
|
364
|
+
* Gate metric, bar = 0: any entry means the scan measured our state, not the page.
|
|
365
|
+
* - state-not-restored: a tool left focus / focus-visible / scroll / selection /
|
|
366
|
+
* injected nodes changed (the scanner restored it)
|
|
367
|
+
* - finding-dropped: a finding sat on an injected (marked) node or on an
|
|
368
|
+
* element a tool left focused; it was not reported
|
|
369
|
+
*/
|
|
370
|
+
interface InternalBug {
|
|
371
|
+
tool: string;
|
|
372
|
+
kind: 'state-not-restored' | 'finding-dropped';
|
|
373
|
+
detail: string;
|
|
374
|
+
selector?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Diagnosis only: what the page DOM did inside this tool's window (the
|
|
377
|
+
* mutation-log slice). Pages mutate themselves, so this is never used to
|
|
378
|
+
* attribute a finding; it is there so a bug is traceable. Capped.
|
|
379
|
+
*/
|
|
380
|
+
mutations?: Array<{
|
|
381
|
+
type: string;
|
|
382
|
+
attr?: string | null;
|
|
383
|
+
target: string;
|
|
384
|
+
added: string[];
|
|
385
|
+
}>;
|
|
386
|
+
}
|
|
321
387
|
interface ScanResult {
|
|
322
388
|
url: string;
|
|
323
389
|
timestamp: string;
|
|
@@ -342,6 +408,18 @@ interface ScanResult {
|
|
|
342
408
|
* disables all contrast checks for the page. Absent = all subsystems healthy.
|
|
343
409
|
*/
|
|
344
410
|
engineWarnings?: string[];
|
|
411
|
+
/**
|
|
412
|
+
* Undecided findings dropped with no verdict: no resolver measurement, no
|
|
413
|
+
* model-free rule and no judge verdict (judge off, failed or silent). For
|
|
414
|
+
* these criteria an absent issue is NOT evidence of a pass. Absent = every
|
|
415
|
+
* undecided finding within the level got a verdict.
|
|
416
|
+
*/
|
|
417
|
+
unsettled?: {
|
|
418
|
+
count: number;
|
|
419
|
+
wcag: string[];
|
|
420
|
+
};
|
|
421
|
+
/** Invariant P: findings / page changes caused by our own tooling. Always present; `[]` = clean. */
|
|
422
|
+
internal_bugs?: InternalBug[];
|
|
345
423
|
/** Engine (core package) version that produced this result — provenance */
|
|
346
424
|
engine?: string;
|
|
347
425
|
/**
|
|
@@ -351,9 +429,10 @@ interface ScanResult {
|
|
|
351
429
|
*/
|
|
352
430
|
incomplete: ScanIssue[];
|
|
353
431
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
432
|
+
* Verdicts on findings the engines could not decide: model-free rules (a
|
|
433
|
+
* measured contrast pass, a decorative or 1x1 image, a reduced-motion rule)
|
|
434
|
+
* and, when configured, the AI judge. `pass` = cleared with a reason,
|
|
435
|
+
* `violation` = promoted into `issues`. Absent when nothing got a verdict.
|
|
357
436
|
*/
|
|
358
437
|
judged?: JudgedEntry[];
|
|
359
438
|
summary: {
|
|
@@ -374,12 +453,20 @@ declare const VIEWPORT_SIZES: Record<ViewportPreset, {
|
|
|
374
453
|
}>;
|
|
375
454
|
interface ScanOptions {
|
|
376
455
|
wcagTags?: string[];
|
|
456
|
+
/** @internal test seam: extra detectors run serially, each in its own state guard. */
|
|
457
|
+
extraSerialDetectors?: FixDetector[];
|
|
377
458
|
/**
|
|
378
459
|
* Highest WCAG conformance level to report. Default 'AA': findings whose
|
|
379
460
|
* criteria are all Level AAA are not reported anywhere (issues, incomplete,
|
|
380
461
|
* summary). 'AAA' opts in (and adds axe's wcag2aaa rules); 'A' keeps Level A only.
|
|
381
462
|
*/
|
|
382
463
|
level?: WcagLevel;
|
|
464
|
+
/**
|
|
465
|
+
* Run axe-core (default true). axe is the only engine for the checks it
|
|
466
|
+
* covers on the ACT test suite: form-field / select / combobox names and
|
|
467
|
+
* valid ARIA roles have no WebAbility detector. false = detector-only scan
|
|
468
|
+
* without those checks.
|
|
469
|
+
*/
|
|
383
470
|
includeAxe?: boolean;
|
|
384
471
|
deep?: boolean;
|
|
385
472
|
deepApiUrl?: string;
|
|
@@ -420,9 +507,10 @@ interface ScanOptions {
|
|
|
420
507
|
* AI judge for findings the engines could not decide. With a model key
|
|
421
508
|
* (local, OpenRouter-compatible) or `apiUrl` (hosted), each such finding
|
|
422
509
|
* becomes a verdict: a violation with a fix in `issues`, or a recorded pass
|
|
423
|
-
* in `judged`. Unset / empty =
|
|
510
|
+
* in `judged`. Unset / empty = no model calls: only the model-free verdicts
|
|
511
|
+
* run, and findings they cannot settle are dropped (listed in `unsettled`).
|
|
424
512
|
*/
|
|
425
513
|
judge?: JudgeOptions;
|
|
426
514
|
}
|
|
427
515
|
|
|
428
|
-
export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type
|
|
516
|
+
export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type InternalBug as I, JUDGE_DEFAULT_TIMEOUT_MS as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type SiteTheme as b, type ScanIssue as c, DEFAULT_JUDGE_BASE_URL as d, DEFAULT_JUDGE_TEXT_MODEL as e, DEFAULT_JUDGE_VISION_MODEL as f, type FixIssueType as g, type IssueImpact as h, type IssueSource as i, JUDGE_SYSTEM_PROMPT as j, type JudgeChatBody as k, type JudgeItem as l, type JudgeOptions as m, type JudgeRequest as n, type JudgeTransport as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webability/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Accessibility scanner engine — WCAG 2.2 AA compliance testing with 200+ rules",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -21,6 +21,18 @@
|
|
|
21
21
|
"import": "./dist/playwright.js",
|
|
22
22
|
"require": "./dist/playwright.cjs",
|
|
23
23
|
"types": "./dist/playwright.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./engine": {
|
|
26
|
+
"import": "./dist/engine.js",
|
|
27
|
+
"require": "./dist/engine.cjs",
|
|
28
|
+
"types": "./dist/engine.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"typesVersions": {
|
|
32
|
+
"*": {
|
|
33
|
+
"engine": [
|
|
34
|
+
"dist/engine.d.ts"
|
|
35
|
+
]
|
|
24
36
|
}
|
|
25
37
|
},
|
|
26
38
|
"files": [
|
|
@@ -30,7 +42,7 @@
|
|
|
30
42
|
"scripts": {
|
|
31
43
|
"build": "npm run gen:dom-kernel && tsup",
|
|
32
44
|
"dev": "tsup --watch",
|
|
33
|
-
"test": "vitest run",
|
|
45
|
+
"test": "WA_STRICT_STATE=1 vitest run",
|
|
34
46
|
"test:watch": "vitest",
|
|
35
47
|
"typecheck": "tsc --noEmit",
|
|
36
48
|
"clean": "rm -rf dist",
|
|
@@ -56,7 +68,8 @@
|
|
|
56
68
|
"directory": "core"
|
|
57
69
|
},
|
|
58
70
|
"dependencies": {
|
|
59
|
-
"@axe-core/playwright": "^4.11.3"
|
|
71
|
+
"@axe-core/playwright": "^4.11.3",
|
|
72
|
+
"css-what": "^6.2.2"
|
|
60
73
|
},
|
|
61
74
|
"peerDependencies": {
|
|
62
75
|
"playwright": "^1.40.0"
|
|
@@ -68,6 +81,7 @@
|
|
|
68
81
|
},
|
|
69
82
|
"devDependencies": {
|
|
70
83
|
"axe-core": "^4.11.3",
|
|
84
|
+
"esbuild": "0.27.7",
|
|
71
85
|
"playwright": "1.59.1",
|
|
72
86
|
"tsup": "^8.3.0",
|
|
73
87
|
"typescript": "^5.6.0",
|