@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
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { c as ScanIssue, m as JudgeOptions, I as InternalBug, q as JudgedEntry, P as PageLike } from './types-fjs6XhzQ.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one engine's contract: two injected drivers in, one result out.
|
|
5
|
+
*
|
|
6
|
+
* runEngine never launches a browser, calls a provider or touches storage on
|
|
7
|
+
* its own (the bundle carries scan()'s URL-string branch, which it never
|
|
8
|
+
* takes). A consumer passes a `Page` (Playwright, Puppeteer, a sandbox) and,
|
|
9
|
+
* for agent tasks, a `Model` (any OpenAI-compatible chat call). Everything the
|
|
10
|
+
* engine reads or writes goes through those two objects. No agent stage runs
|
|
11
|
+
* yet: the rubric agent for judgment criteria is its own step.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal browser page. A Playwright page needs a thin adapter
|
|
16
|
+
* (`fromPlaywright`: its `evaluate` takes one argument). Optional members widen what the engine can
|
|
17
|
+
* do; each absent one is named in `stats.warnings`, never guessed around.
|
|
18
|
+
*
|
|
19
|
+
* Isolation is the driver's: for `html` input the engine removes `<script>`
|
|
20
|
+
* (HTML and SVG) and meta refresh but keeps inline handler attributes (detectors read them),
|
|
21
|
+
* so the driver must give the page no egress it would not give a scanned site
|
|
22
|
+
* (the hosted MCP's SSRF route, a sandbox), or turn JavaScript off.
|
|
23
|
+
*/
|
|
24
|
+
interface Page {
|
|
25
|
+
/** A Response-like return (`status()`) is read for the main document's HTTP status. */
|
|
26
|
+
goto(url: string, opts?: {
|
|
27
|
+
waitUntil?: any;
|
|
28
|
+
timeout?: number;
|
|
29
|
+
}): Promise<unknown>;
|
|
30
|
+
evaluate<R>(pageFunction: string | ((...args: any[]) => R | Promise<R>), ...args: any[]): Promise<R>;
|
|
31
|
+
/** Must honour `clip`, `fullPage` and `animations`: pixel contrast crops by `clip`. */
|
|
32
|
+
screenshot(opts?: {
|
|
33
|
+
type?: 'png';
|
|
34
|
+
encoding?: 'base64';
|
|
35
|
+
clip?: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
};
|
|
41
|
+
fullPage?: boolean;
|
|
42
|
+
animations?: 'disabled' | 'allow';
|
|
43
|
+
}): Promise<Uint8Array | string>;
|
|
44
|
+
/** focusVisible presses Tab to put the browser in keyboard modality. */
|
|
45
|
+
keyboard: {
|
|
46
|
+
press(key: string): Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
/** Wait for the document's `load` event (render settle). Absent: settle waits on animations only, so late iframes/media may be missed. */
|
|
49
|
+
waitForLoadState?(state: 'load', opts?: {
|
|
50
|
+
timeout?: number;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/** Current URL. Absent: the input URL is used. */
|
|
53
|
+
url?(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Serve `html` as the document at `url` and navigate there (no network fetch
|
|
56
|
+
* of the page itself). Absent: a snapshot stays on about:blank with a
|
|
57
|
+
* `<base href>`, and `location` is about:blank.
|
|
58
|
+
*/
|
|
59
|
+
loadHtml?(html: string, url: string): Promise<void>;
|
|
60
|
+
/** Script injection (Playwright/Puppeteer). Absent: string `evaluate` is used. */
|
|
61
|
+
addScriptTag?(opts: {
|
|
62
|
+
content?: string;
|
|
63
|
+
url?: string;
|
|
64
|
+
}): Promise<unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* The live Playwright Page. axe's frame-aware run (AxeBuilder) needs it to
|
|
67
|
+
* scan inside iframes. Absent: axe runs in the top document only.
|
|
68
|
+
*/
|
|
69
|
+
playwrightPage?(): unknown;
|
|
70
|
+
}
|
|
71
|
+
/** One OpenAI-compatible chat call. `signal` aborts the HTTP request at the engine's deadline. */
|
|
72
|
+
interface ModelRequest {
|
|
73
|
+
model?: string;
|
|
74
|
+
messages: any[];
|
|
75
|
+
tools?: any[];
|
|
76
|
+
max_tokens: number;
|
|
77
|
+
temperature?: number;
|
|
78
|
+
reasoning_effort?: 'low' | 'medium' | 'high';
|
|
79
|
+
}
|
|
80
|
+
interface ModelResponse {
|
|
81
|
+
message: {
|
|
82
|
+
content?: string | null;
|
|
83
|
+
tool_calls?: any[];
|
|
84
|
+
};
|
|
85
|
+
/** OpenAI usage; `cost` (USD) when the gateway reports it. */
|
|
86
|
+
usage?: {
|
|
87
|
+
prompt_tokens?: number;
|
|
88
|
+
completion_tokens?: number;
|
|
89
|
+
prompt_tokens_details?: {
|
|
90
|
+
cached_tokens?: number;
|
|
91
|
+
};
|
|
92
|
+
cost?: number;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
interface Model {
|
|
96
|
+
chat(req: ModelRequest, opts?: {
|
|
97
|
+
signal?: AbortSignal;
|
|
98
|
+
}): Promise<ModelResponse>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* What to scan. `url` alone: the engine loads it on the page. `html`: a DOM
|
|
102
|
+
* snapshot (the widget Worker's input) loaded into the page, with `url` as its
|
|
103
|
+
* base and the source of the fix records' page path.
|
|
104
|
+
*/
|
|
105
|
+
type EngineInput = {
|
|
106
|
+
url: string;
|
|
107
|
+
html?: undefined;
|
|
108
|
+
} | {
|
|
109
|
+
html: string;
|
|
110
|
+
url?: string;
|
|
111
|
+
};
|
|
112
|
+
interface EngineOptions {
|
|
113
|
+
/** Highest WCAG level reported. Default 'AA'. */
|
|
114
|
+
level?: 'A' | 'AA' | 'AAA';
|
|
115
|
+
/** HTTP status of the main document when the caller navigated. */
|
|
116
|
+
mainStatus?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Knobs of the deterministic layer (core scan()): scope to a subtree,
|
|
119
|
+
* dismiss cookie banners first (default false), the axe tags, the render
|
|
120
|
+
* settle, and core's in-scan AI judge for undecided findings. The judge
|
|
121
|
+
* makes its own HTTP calls (not through `Model`); leave it unset for a run
|
|
122
|
+
* with no model traffic.
|
|
123
|
+
*/
|
|
124
|
+
scan?: {
|
|
125
|
+
rootSelector?: string;
|
|
126
|
+
dismissModals?: boolean;
|
|
127
|
+
judge?: JudgeOptions;
|
|
128
|
+
/** axe-core tags (default: core's WCAG A/AA set). */
|
|
129
|
+
wcagTags?: string[];
|
|
130
|
+
/** scan()'s render settle: false skips it (a DOM with no layout or animation). */
|
|
131
|
+
settle?: boolean;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* One fix record. The first five keys are the served manifest's wire keys
|
|
136
|
+
* (widget reads `sel`/`attr`/`val`/`mode`/`p`); the rest are provenance the
|
|
137
|
+
* Worker and the safety lookups need.
|
|
138
|
+
*/
|
|
139
|
+
interface EngineFix {
|
|
140
|
+
sel: string;
|
|
141
|
+
/** The selector exactly as the producing engine emitted it, before any rewrite. Equal to `sel` when nothing rewrote it. */
|
|
142
|
+
originalSel: string;
|
|
143
|
+
attr: string;
|
|
144
|
+
val: string;
|
|
145
|
+
/** Served manifest mode. Engine fixes are plain attribute writes: 'attr'. */
|
|
146
|
+
mode: 'attr';
|
|
147
|
+
/** Page path the fix is scoped to (from the input URL), '/' when the input had none. */
|
|
148
|
+
p: string;
|
|
149
|
+
issueType: string;
|
|
150
|
+
wcag: string;
|
|
151
|
+
/** What the fix rests on: the finding's message and the element markup. */
|
|
152
|
+
evidence: string;
|
|
153
|
+
engineVersion: string;
|
|
154
|
+
}
|
|
155
|
+
/** One reported finding (scan()'s issue row). */
|
|
156
|
+
type EngineIssue = ScanIssue;
|
|
157
|
+
|
|
158
|
+
interface EngineStats {
|
|
159
|
+
engineVersion: string;
|
|
160
|
+
input: 'url' | 'snapshot';
|
|
161
|
+
ms: number;
|
|
162
|
+
deterministic: {
|
|
163
|
+
issues: number;
|
|
164
|
+
bySource: Record<string, number>;
|
|
165
|
+
engineWarnings: string[];
|
|
166
|
+
};
|
|
167
|
+
fixes: {
|
|
168
|
+
candidates: number;
|
|
169
|
+
emitted: number;
|
|
170
|
+
rejectedByValidator: number;
|
|
171
|
+
rejectedByHidingGuard: number;
|
|
172
|
+
rejected: Array<{
|
|
173
|
+
sel: string;
|
|
174
|
+
attr: string;
|
|
175
|
+
why: string;
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
/** No agent stage yet: always `ran: false`, and `skipped` says why. */
|
|
179
|
+
agent: {
|
|
180
|
+
ran: false;
|
|
181
|
+
skipped: string;
|
|
182
|
+
};
|
|
183
|
+
/** Storage operations the engine performed. Always 0: the package has no storage. */
|
|
184
|
+
io: {
|
|
185
|
+
dbRows: 0;
|
|
186
|
+
r2Gets: 0;
|
|
187
|
+
r2Puts: 0;
|
|
188
|
+
kvOps: 0;
|
|
189
|
+
};
|
|
190
|
+
/** Degraded stages (a failed load, a guard that could not run). A result with errors is partial. */
|
|
191
|
+
errors: string[];
|
|
192
|
+
/** Capabilities the injected Page lacked and what was skipped because of it. */
|
|
193
|
+
warnings: string[];
|
|
194
|
+
}
|
|
195
|
+
interface EngineResult {
|
|
196
|
+
url: string;
|
|
197
|
+
issues: EngineIssue[];
|
|
198
|
+
incomplete: EngineIssue[];
|
|
199
|
+
fixes: EngineFix[];
|
|
200
|
+
internal_bugs: InternalBug[];
|
|
201
|
+
stats: EngineStats;
|
|
202
|
+
/** Verdicts of core's in-scan judge (`opts.scan.judge`) on findings the engines could not decide. */
|
|
203
|
+
judged?: JudgedEntry[];
|
|
204
|
+
/** Set when the target was a bot challenge, auth wall or HTTP error: nothing real was scanned. */
|
|
205
|
+
blocked?: {
|
|
206
|
+
reason: string;
|
|
207
|
+
signal?: string;
|
|
208
|
+
status?: number;
|
|
209
|
+
vendor?: string;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** The served manifest wire keys, in order. */
|
|
213
|
+
declare const SERVED_FIX_KEYS: readonly ["sel", "attr", "val", "mode", "p"];
|
|
214
|
+
|
|
215
|
+
declare function runEngine(args: {
|
|
216
|
+
page: Page;
|
|
217
|
+
model?: Model;
|
|
218
|
+
input: EngineInput;
|
|
219
|
+
opts?: EngineOptions;
|
|
220
|
+
}): Promise<EngineResult>;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Driver adapters: the injected Page as core's PageLike, and a DOM snapshot
|
|
224
|
+
* loaded into it.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* core's scan() runs on a PageLike (evaluate + addScriptTag + url). A driver
|
|
229
|
+
* without addScriptTag gets script content through a string `evaluate`. A
|
|
230
|
+
* script by URL is refused; no scan() stage loads one, so nothing is lost.
|
|
231
|
+
*/
|
|
232
|
+
declare function toPageLike(page: Page, fallbackUrl: string): PageLike;
|
|
233
|
+
/**
|
|
234
|
+
* Load a DOM snapshot as the page's document, with no network fetch of the
|
|
235
|
+
* page itself. The HTML is parsed by DOMParser on about:blank; `<script>`
|
|
236
|
+
* elements of any namespace (HTML or SVG) and `<meta http-equiv=refresh>`
|
|
237
|
+
* are removed, so the snapshot cannot run its scripts or navigate the page on
|
|
238
|
+
* load.
|
|
239
|
+
*
|
|
240
|
+
* Inline event-handler attributes (`onclick`, `onerror`…) are KEPT: detectors
|
|
241
|
+
* read their text (missingRoles, linkWarnings, targetSize, timingAndInput). An
|
|
242
|
+
* `<img onerror>` can therefore still run when the snapshot loads; isolating
|
|
243
|
+
* that (no egress, or JavaScript off) is the driver's job, as stated on
|
|
244
|
+
* `Page`.
|
|
245
|
+
*
|
|
246
|
+
* With `baseUrl` and a driver `loadHtml`, the cleaned HTML is served at
|
|
247
|
+
* `baseUrl`, so `location` is the live page's (detectors compare links with
|
|
248
|
+
* `location.origin`/`pathname`). Without `loadHtml` the document stays on
|
|
249
|
+
* about:blank with a `<base href>`; the engine names that in `stats.warnings`.
|
|
250
|
+
*/
|
|
251
|
+
declare function loadSnapshot(page: Page, html: string, baseUrl?: string): Promise<void>;
|
|
252
|
+
/** The Playwright page members the adapter reads (structural: no playwright import). */
|
|
253
|
+
interface PlaywrightPageLike {
|
|
254
|
+
goto(url: string, opts?: any): Promise<any>;
|
|
255
|
+
evaluate(fn: any, arg?: any): Promise<any>;
|
|
256
|
+
screenshot(opts?: any): Promise<Uint8Array>;
|
|
257
|
+
keyboard: {
|
|
258
|
+
press(key: string): Promise<void>;
|
|
259
|
+
};
|
|
260
|
+
waitForLoadState?(state: 'load', opts?: {
|
|
261
|
+
timeout?: number;
|
|
262
|
+
}): Promise<void>;
|
|
263
|
+
url(): string;
|
|
264
|
+
addScriptTag?(opts: {
|
|
265
|
+
content?: string;
|
|
266
|
+
url?: string;
|
|
267
|
+
}): Promise<unknown>;
|
|
268
|
+
route?(url: (u: URL) => boolean, handler: (route: any) => unknown): Promise<void>;
|
|
269
|
+
unroute?(url: (u: URL) => boolean, handler?: (route: any) => unknown): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* A Playwright page as the engine's Page. Playwright's evaluate takes one
|
|
273
|
+
* argument. With `route`, a snapshot is served at its own URL (`loadHtml`),
|
|
274
|
+
* so `location` is the live page's.
|
|
275
|
+
*/
|
|
276
|
+
declare function fromPlaywright(p: PlaywrightPageLike): Page;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Fix Manifest Validator — Three-Layer Safety Architecture
|
|
280
|
+
*
|
|
281
|
+
* Every fix passes through three layers before reaching the DB:
|
|
282
|
+
*
|
|
283
|
+
* Layer 1: Structural validation (selector, attribute, value safety)
|
|
284
|
+
* Layer 2: Protected element check (don't touch third-party/admin/consent UI)
|
|
285
|
+
* Layer 3: Risk classification (additive vs modifier vs destructive)
|
|
286
|
+
*
|
|
287
|
+
* Layer 3 output feeds into the repository's status logic:
|
|
288
|
+
* - additive → normal confidence gate (>= 0.85 → approved)
|
|
289
|
+
* - modifier → always 'pending' (human review required)
|
|
290
|
+
* - destructive → rejected (unless source is 'manual')
|
|
291
|
+
*/
|
|
292
|
+
interface ValidationResult {
|
|
293
|
+
ok: boolean;
|
|
294
|
+
reason?: string;
|
|
295
|
+
}
|
|
296
|
+
type FixRiskTier = 'additive' | 'modifier' | 'destructive';
|
|
297
|
+
interface FixClassification {
|
|
298
|
+
tier: FixRiskTier;
|
|
299
|
+
reason: string;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Validate a `role` attribute value.
|
|
303
|
+
* - case-sensitive: must be lowercase (rejects "Main", "Breadcrumb", "Navigation")
|
|
304
|
+
* - every space-separated token must be a real concrete ARIA role
|
|
305
|
+
* - empty / whitespace-only is invalid
|
|
306
|
+
*/
|
|
307
|
+
declare function validateRoleValue(value: string): ValidationResult;
|
|
308
|
+
/**
|
|
309
|
+
* The one fix the `<html>` root takes: `lang` (WCAG 3.1.1 — the document
|
|
310
|
+
* language lives on `<html>` and nowhere else). afe1654ed rejected every bare
|
|
311
|
+
* `html` selector as structural hardening (its case: `html { style:
|
|
312
|
+
* filter:invert(1) }` repaints every page); `lang` writes no style, no markup
|
|
313
|
+
* and no behaviour, so it is the single exception. Every other attribute on
|
|
314
|
+
* `html` stays rejected. Exact attribute name, no case folding.
|
|
315
|
+
*/
|
|
316
|
+
declare function isHtmlLangFix(selector: string | null | undefined, attribute: string | null | undefined): boolean;
|
|
317
|
+
/**
|
|
318
|
+
* Selector check for a fix whose attribute is known. `html` + `lang` passes
|
|
319
|
+
* (the root is one element, never protected UI); everything else takes
|
|
320
|
+
* validateSelector, where a bare `html` is still rejected.
|
|
321
|
+
*/
|
|
322
|
+
declare function validateSelectorForAttribute(selector: string, attribute: string, protectedScope?: ProtectedScope): ValidationResult;
|
|
323
|
+
/**
|
|
324
|
+
* Would the browser's querySelectorAll parse this selector? (LEDGER row 78 —
|
|
325
|
+
* served selectors that throw are swallowed per fix by the widget and stay
|
|
326
|
+
* approved forever.) Two layers: the character/escape rules below refuse an
|
|
327
|
+
* unescaped `/`, `%`, `!` or a Tailwind `:variant` exactly as the browser
|
|
328
|
+
* does; then a real tokeniser (css-what) refuses unbalanced brackets, an
|
|
329
|
+
* unterminated string, an empty `:not()`, a dangling `#`, and `astParses`
|
|
330
|
+
* refuses what css-what tolerates but the browser does not (`#hero >`,
|
|
331
|
+
* `a||b`, `li:nth-child(2n+)`, `button::part()`).
|
|
332
|
+
*/
|
|
333
|
+
declare function selectorParses(selector: string): boolean;
|
|
334
|
+
/** Which protected-element registry a writer consults. The scan path runs the
|
|
335
|
+
* full registry against a DOM it rendered; the ingest path keeps the narrower
|
|
336
|
+
* admin/overlay check because the full registry's SUBSTRING vendor patterns
|
|
337
|
+
* (crisp-, hubspot-) false-match attribute VALUES in live approved fixes
|
|
338
|
+
* (img[src=".../apple-cranberry-crisp-…jpg"], body.hubspot-disable-focus-styles). */
|
|
339
|
+
type ProtectedScope = 'full' | 'dead-region';
|
|
340
|
+
declare function validateSelector(selector: string, protectedScope?: ProtectedScope): ValidationResult;
|
|
341
|
+
declare function validateAttribute(attr: string): ValidationResult;
|
|
342
|
+
declare function validateTextValue(value: string, attribute?: string): ValidationResult;
|
|
343
|
+
/**
|
|
344
|
+
* Classify a fix into a risk tier:
|
|
345
|
+
* - additive: adds information that wasn't there (aria-label, alt, role, scope, etc.)
|
|
346
|
+
* - modifier: changes visibility/interactivity (aria-hidden, tabindex, style)
|
|
347
|
+
* - destructive: removes or empties functional attributes
|
|
348
|
+
*/
|
|
349
|
+
declare function classifyFixRisk(fix: {
|
|
350
|
+
attribute: string;
|
|
351
|
+
value: string;
|
|
352
|
+
}): FixClassification;
|
|
353
|
+
/**
|
|
354
|
+
* `issueType` + `context` are optional: when the caller has the element's
|
|
355
|
+
* stored context (visible text), the WCAG 2.5.3 Label-in-Name rule runs on
|
|
356
|
+
* aria-label fixes (see labelInNameRule.ts). Without them the check is a no-op.
|
|
357
|
+
*/
|
|
358
|
+
declare function validateFix(fix: {
|
|
359
|
+
selector: string;
|
|
360
|
+
attribute: string;
|
|
361
|
+
value: string;
|
|
362
|
+
issueType?: string | null;
|
|
363
|
+
context?: string | null;
|
|
364
|
+
}, protectedScope?: ProtectedScope): ValidationResult;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Hiding-hazard guard
|
|
368
|
+
* ───────────────────
|
|
369
|
+
* A single, detector-agnostic guard that prevents the scan from EVER emitting a
|
|
370
|
+
* fix that would remove an interactive control from the accessibility tree.
|
|
371
|
+
*
|
|
372
|
+
* The hazard: `aria-hidden="true"`, `role="presentation"`, or `role="none"` on
|
|
373
|
+
* an element that is itself interactive (a real `<a href>`, button, form
|
|
374
|
+
* control, interactive ARIA role, or `tabindex >= 0`) — or that CONTAINS a
|
|
375
|
+
* focusable descendant. Applied to, say, a "Sign up" CTA link, this hides the
|
|
376
|
+
* button from screen-reader users AND leaves it keyboard-focusable-but-invisible
|
|
377
|
+
* (a WCAG 4.1.2 hidden-focusable). That is the exact "overlay makes it worse"
|
|
378
|
+
* failure — the worst thing an accessibility product can ship.
|
|
379
|
+
*
|
|
380
|
+
* Several detectors CAN produce this in edge cases (decorative-icon size/class
|
|
381
|
+
* heuristics, sr-only-container keyboard-trap heuristics, adjacent-identical-
|
|
382
|
+
* link dedup). Rather than patch each one, this guard runs once over the merged
|
|
383
|
+
* output against the LIVE DOM, so it covers every detector — present and future
|
|
384
|
+
* — from one place. Correctly-decorative targets (non-interactive, no focusable
|
|
385
|
+
* descendants) are untouched.
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
/** The fields of a fix row the guard reads. */
|
|
389
|
+
interface HidingCandidate {
|
|
390
|
+
selector: string;
|
|
391
|
+
attribute?: string;
|
|
392
|
+
fixValue?: string;
|
|
393
|
+
}
|
|
394
|
+
/** Does this fix remove its target (and subtree) from the accessibility tree? */
|
|
395
|
+
declare function isHidingFix(attribute: string | undefined, fixValue: string | undefined): boolean;
|
|
396
|
+
/**
|
|
397
|
+
* Minimal element view — satisfied by a real DOM Element in the browser and by
|
|
398
|
+
* a fake in tests. The browser pass mirrors this rule's LOGIC (page.evaluate
|
|
399
|
+
* can't reference this import), but the role list + focusable selector are
|
|
400
|
+
* passed IN as arguments, so those constants have a single source of truth.
|
|
401
|
+
*/
|
|
402
|
+
interface HazardElementView {
|
|
403
|
+
tagName: string;
|
|
404
|
+
getAttribute(name: string): string | null;
|
|
405
|
+
hasAttribute(name: string): boolean;
|
|
406
|
+
querySelector(sel: string): unknown | null;
|
|
407
|
+
querySelectorAll?(sel: string): ArrayLike<HazardElementView>;
|
|
408
|
+
textContent?: string | null;
|
|
409
|
+
isContentEditable?: boolean;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* True when hiding this element would swallow a control: it is itself
|
|
413
|
+
* interactive/focusable, OR it contains a focusable descendant, OR it is or
|
|
414
|
+
* contains visually-hidden text (the sr-only pattern). That text exists only
|
|
415
|
+
* for screen-reader users, often as an icon's or link's only name; hiding it
|
|
416
|
+
* leaves them nothing (B13). `isVisuallyHidden` is the DOM fact (the live
|
|
417
|
+
* pass measures geometry and clipping); without it the third check is off.
|
|
418
|
+
*/
|
|
419
|
+
declare function isInteractiveOrContainsFocusable(el: HazardElementView, isVisuallyHidden?: (e: HazardElementView) => boolean): boolean;
|
|
420
|
+
/**
|
|
421
|
+
* Drop every hiding fix whose live target is interactive or contains a
|
|
422
|
+
* focusable descendant. One page.evaluate over just the hiding candidates.
|
|
423
|
+
* Fails CLOSED for hiding fixes only: when the check cannot run, every hiding
|
|
424
|
+
* candidate is dropped (other fixes pass) and `onError` gets the reason.
|
|
425
|
+
*/
|
|
426
|
+
declare function dropHazardousHidingFixes<T extends HidingCandidate>(page: Pick<Page, 'evaluate'>, issues: T[], onError?: (err: Error) => void): Promise<T[]>;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The subset of INTERACTIVE_ROLES that behave like a native form-control
|
|
430
|
+
* input (something a <fieldset> groups) — excludes navigational/structural
|
|
431
|
+
* roles (button, link, menuitem*, tab, option, treeitem, gridcell) that are
|
|
432
|
+
* interactive but not "controls" a fieldset legend would describe.
|
|
433
|
+
*
|
|
434
|
+
* fieldsetLegend.detector.ts's ARIA form-control count must inline an
|
|
435
|
+
* identical literal rather than import this — its detector function is
|
|
436
|
+
* serialized via page.evaluate/toString and cannot reference an imported
|
|
437
|
+
* binding at runtime (same constraint as the kernel). Only
|
|
438
|
+
* test/fieldsetLegend.formControlRoles.test.ts actually reads this export,
|
|
439
|
+
* to pin that inline literal against drift.
|
|
440
|
+
*/
|
|
441
|
+
declare const FORM_CONTROL_ROLES: Set<string>;
|
|
442
|
+
/** Is the visible text a label the rule applies to? Shared with the vector. */
|
|
443
|
+
declare const isLabelText: (text: string) => boolean;
|
|
444
|
+
/** Does `value` contain `text` as a whole phrase (see labelContainsText in the kernel)? */
|
|
445
|
+
declare const labelContainsText: (value: string, text: string) => boolean;
|
|
446
|
+
/** Widget normalisation: trim, collapse whitespace. Case-folding happens at compare time. */
|
|
447
|
+
declare const normalizeVisibleText: (text: string) => string;
|
|
448
|
+
/**
|
|
449
|
+
* The same contract as browser-side JS for the scan detector, which runs under
|
|
450
|
+
* page.evaluate and cannot import (labelInName.detector.ts; injected by
|
|
451
|
+
* index.ts with the other helpers, declared in browser-globals.d.ts). Built
|
|
452
|
+
* from the very functions above, so there is one implementation, not a copy.
|
|
453
|
+
*/
|
|
454
|
+
declare const LABEL_IN_NAME_JS: string;
|
|
455
|
+
interface LabelInNameInput {
|
|
456
|
+
attribute: string;
|
|
457
|
+
value: string;
|
|
458
|
+
issueType?: string | null;
|
|
459
|
+
/** The stored `fix_manifests.context` (JSON from the widget/Worker, or the
|
|
460
|
+
* scan detectors' `visible: … | aria-label: …` pipe string). */
|
|
461
|
+
context?: string | null;
|
|
462
|
+
}
|
|
463
|
+
interface LabelInNameResult {
|
|
464
|
+
ok: boolean;
|
|
465
|
+
reason?: string;
|
|
466
|
+
}
|
|
467
|
+
/** Lowercase, strip diacritics, collapse everything that is not a letter or digit to one space. */
|
|
468
|
+
declare function foldLabel(s: string): string;
|
|
469
|
+
/** Does `haystack` contain `needle` as a whole phrase after folding? */
|
|
470
|
+
declare function foldContains(haystack: string, needle: string): boolean;
|
|
471
|
+
interface ParsedContext {
|
|
472
|
+
tag: string;
|
|
473
|
+
role: string;
|
|
474
|
+
text: string;
|
|
475
|
+
/** The element's CURRENT accessible name as the detector saw it (Worker `currentAriaLabel` / `attrs['aria-label']`, scan-detector `aria-label:`); '' when the context carries none. */
|
|
476
|
+
ariaLabel: string;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The stored `fix_manifests.context` in its three generations: Worker JSON
|
|
480
|
+
* (optionally wrapped as `{original: "<json>", validationFailure}` by the AI
|
|
481
|
+
* validator), and the scan detectors' `visible: … | aria-label: …` pipe string.
|
|
482
|
+
*/
|
|
483
|
+
declare function parseContext(context: string | null | undefined): ParsedContext;
|
|
484
|
+
/**
|
|
485
|
+
* Does this aria-label fix satisfy Label in Name? `ok:true` also when the rule
|
|
486
|
+
* does not apply (other attribute, non-interactive element, no visible text or
|
|
487
|
+
* symbol-only text) — callers treat only `ok:false` as a reject.
|
|
488
|
+
*/
|
|
489
|
+
declare function labelInNameCheck(fix: LabelInNameInput): LabelInNameResult;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Page language detection for the missing `<html lang>` fix (WCAG 3.1.1).
|
|
493
|
+
*
|
|
494
|
+
* One file, two byte-identical copies: fix-manifest-api src/detectors/pageLanguage.ts
|
|
495
|
+
* and platform api/services/fixDetectors/pageLanguage.ts. A pinned-sha256 test
|
|
496
|
+
* in each repo (pageLanguage-drift) fails on any edit to one copy alone: change
|
|
497
|
+
* both, then update both pins in the same pair of PRs.
|
|
498
|
+
*
|
|
499
|
+
* The fix value is the page's REAL language, or no fix at all. A wrong `lang`
|
|
500
|
+
* makes the screen reader read the whole page with the wrong voice, which is
|
|
501
|
+
* worse than no `lang`. Every `html[lang]` row in prod carried "en" (72/72,
|
|
502
|
+
* 15 of them on German, Dutch, Spanish, French, Danish, Chinese and Italian
|
|
503
|
+
* pages, 2026-10-09) because the AI fill only saw the page title and domain.
|
|
504
|
+
*
|
|
505
|
+
* Inputs are facts the pipeline already holds at scan/ingest time: the page
|
|
506
|
+
* URL, the page text, and (when the caller can read them) the language the
|
|
507
|
+
* page declares in xml:lang / content-language / og:locale / body[lang] /
|
|
508
|
+
* a self-referencing hreflang. Pure function: no network, no AI, no per-site
|
|
509
|
+
* data. Runs once per page at detection time; the widget only applies the
|
|
510
|
+
* stored value.
|
|
511
|
+
*
|
|
512
|
+
* Decision:
|
|
513
|
+
* 1. Text decides when it is confident (function-word frequency for Latin
|
|
514
|
+
* script languages, the writing system for scripts only one language
|
|
515
|
+
* uses). Any declared or URL signal that disagrees → null.
|
|
516
|
+
* 2. Text not confident: declared signals that all agree decide, unless
|
|
517
|
+
* the text leans to another language or rules the declared one out.
|
|
518
|
+
* 3. A URL code (language subdomain or first path segment) decides only
|
|
519
|
+
* when the text leans the same way and does not rule it out.
|
|
520
|
+
* 4. Otherwise null — the caller emits NO row.
|
|
521
|
+
*
|
|
522
|
+
* Function-word evidence is measured against ALL page tokens, not only the
|
|
523
|
+
* tokens we recognise. A language's words make up 15–30% of the tokens of a
|
|
524
|
+
* real page in that language (home pages, nav included: 0.157–0.321 on 18
|
|
525
|
+
* live pages, 2026-10-10). A page in a language with no list here (Romanian,
|
|
526
|
+
* Indonesian, Turkish, Finnish, Hungarian) shares a few short words with the
|
|
527
|
+
* listed ones but reaches ≈0.10 at most for any of them, so it is never confident —
|
|
528
|
+
* and it rules out every listed language, so a declared "en" on such a page
|
|
529
|
+
* is not trusted either. Languages close enough to a listed one to clear the
|
|
530
|
+
* floor on their own (Catalan ~ Spanish, Galician ~ Portuguese, Afrikaans ~
|
|
531
|
+
* Dutch) carry their own list, so the words that tell them apart decide.
|
|
532
|
+
*/
|
|
533
|
+
type LangSource = 'text' | 'declared' | 'url';
|
|
534
|
+
interface PageLanguageInput {
|
|
535
|
+
/** Page URL (or the canonical page key). */
|
|
536
|
+
url?: string | null;
|
|
537
|
+
/** Visible page text (any length; only the first 20k chars are read). */
|
|
538
|
+
text?: string | null;
|
|
539
|
+
/** Languages the page declares outside `<html lang>`: xml:lang, content-language, meta language, og:locale, body[lang], self hreflang. */
|
|
540
|
+
declared?: Array<string | null | undefined>;
|
|
541
|
+
}
|
|
542
|
+
interface PageLanguageResult {
|
|
543
|
+
/** Primary BCP-47 subtag, lowercase ("de", "nl", "zh"). */
|
|
544
|
+
lang: string;
|
|
545
|
+
source: LangSource;
|
|
546
|
+
}
|
|
547
|
+
interface TextVerdict {
|
|
548
|
+
/** Language the text points to, if any. */
|
|
549
|
+
lean: string | null;
|
|
550
|
+
/** True when the lean is strong enough to decide alone. */
|
|
551
|
+
confident: boolean;
|
|
552
|
+
/** Listed languages the text rules out: enough tokens, and their function words stay under MIN_SHARE of them. */
|
|
553
|
+
ruledOut: Set<string>;
|
|
554
|
+
}
|
|
555
|
+
/** Primary subtag of a BCP-47-ish tag ("de-DE", "de_DE", "zh-Hans" → "de"/"zh"); '' when it is not a language code. */
|
|
556
|
+
declare function primaryLanguage(tag: string | null | undefined): string;
|
|
557
|
+
/** Language code carried by the URL: a language subdomain ("fr.shop.de") or first path segment ("/de/", "/zh-hans/"). */
|
|
558
|
+
declare function languageFromUrl(url: string | null | undefined): string;
|
|
559
|
+
/** What the page text says about its language. */
|
|
560
|
+
declare function languageFromText(raw: string | null | undefined): TextVerdict;
|
|
561
|
+
/** The page's language, or null when nothing settles it (→ emit no fix). */
|
|
562
|
+
declare function detectPageLanguage(input: PageLanguageInput): PageLanguageResult | null;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Selector Quality Scoring
|
|
566
|
+
*
|
|
567
|
+
* Scores CSS selectors on specificity and stability. Fixes with low-quality
|
|
568
|
+
* selectors are either rejected or forced to pending based on score.
|
|
569
|
+
*
|
|
570
|
+
* A selector that can match unintended elements is dangerous — it can hide
|
|
571
|
+
* content, strip functionality, or inject wrong labels on the wrong elements.
|
|
572
|
+
*/
|
|
573
|
+
interface SelectorQualityResult {
|
|
574
|
+
score: number;
|
|
575
|
+
reasons: string[];
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Bare element tags that are too broad when used as the sole selector.
|
|
579
|
+
* `iframe` as a selector matches every iframe on the page.
|
|
580
|
+
*
|
|
581
|
+
* Exported so anything that judges a selector PART by this same universe
|
|
582
|
+
* (e.g. a comma-separated template row) imports this set instead of keeping
|
|
583
|
+
* a second hand-written copy that can silently drift (Pullfrog, #1025).
|
|
584
|
+
*/
|
|
585
|
+
declare const BARE_TAGS: Set<string>;
|
|
586
|
+
declare function scoreSelectorQuality(selector: string): SelectorQualityResult;
|
|
587
|
+
|
|
588
|
+
export { BARE_TAGS, type EngineFix, type EngineInput, type EngineIssue, type EngineOptions, type EngineResult, type EngineStats, FORM_CONTROL_ROLES, type HazardElementView, InternalBug, LABEL_IN_NAME_JS, type Model, type ModelRequest, type ModelResponse, type Page, type PlaywrightPageLike, type ProtectedScope, SERVED_FIX_KEYS, classifyFixRisk, detectPageLanguage, dropHazardousHidingFixes, foldContains, foldLabel, fromPlaywright, isHidingFix, isHtmlLangFix, isInteractiveOrContainsFocusable, isLabelText, labelContainsText, labelInNameCheck, languageFromText, languageFromUrl, loadSnapshot, normalizeVisibleText, parseContext, primaryLanguage, runEngine, scoreSelectorQuality, selectorParses, toPageLike, validateAttribute, validateFix, validateRoleValue, validateSelector, validateSelectorForAttribute, validateTextValue };
|