@diegovelasquezweb/a11y-engine 0.10.1 → 0.11.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/CHANGELOG.md +16 -0
- package/assets/remediation/intelligence.mjs +4818 -1
- package/assets/scanning/cdp-checks.mjs +60 -1
- package/docs/api-reference.md +22 -2
- package/docs/architecture.md +24 -1
- package/docs/intelligence.md +2 -2
- package/docs/outputs.md +5 -1
- package/package.json +1 -1
- package/src/ai/claude.mjs +66 -5
- package/src/enrichment/analyzer.mjs +30 -0
- package/src/index.d.mts +58 -2
- package/src/index.mjs +15 -3
- package/src/pipeline/dom-scanner.mjs +242 -21
|
@@ -1 +1,60 @@
|
|
|
1
|
-
export default {
|
|
1
|
+
export default {
|
|
2
|
+
"interactiveRoles": ["button","link","textbox","combobox","listbox","menuitem","tab","checkbox","radio","switch","slider"],
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "cdp-missing-accessible-name",
|
|
6
|
+
"condition": "interactive-no-name",
|
|
7
|
+
"impact": "serious",
|
|
8
|
+
"tags": ["wcag2a","wcag412","cdp-check"],
|
|
9
|
+
"help": "Interactive elements must have an accessible name",
|
|
10
|
+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/button-name",
|
|
11
|
+
"description": "Interactive element with role \"{{role}}\" has no accessible name",
|
|
12
|
+
"failureMessage": "Element with role \"{{role}}\" has no accessible name in the accessibility tree",
|
|
13
|
+
"axeEquivalents": ["button-name","link-name","input-name","aria-command-name"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "cdp-aria-hidden-focusable",
|
|
17
|
+
"condition": "hidden-focusable",
|
|
18
|
+
"impact": "serious",
|
|
19
|
+
"tags": ["wcag2a","wcag412","cdp-check"],
|
|
20
|
+
"help": "aria-hidden elements must not be focusable",
|
|
21
|
+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/aria-hidden-focus",
|
|
22
|
+
"description": "Focusable element with role \"{{role}}\" is aria-hidden",
|
|
23
|
+
"failureMessage": "Focusable element with role \"{{role}}\" is hidden from the accessibility tree",
|
|
24
|
+
"axeEquivalents": ["aria-hidden-focus"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "cdp-autoplay-media",
|
|
28
|
+
"condition": "dom-eval",
|
|
29
|
+
"impact": "serious",
|
|
30
|
+
"tags": ["wcag2a","wcag142","wcag222","cdp-check"],
|
|
31
|
+
"help": "Media elements must not autoplay without user control",
|
|
32
|
+
"helpUrl": "https://www.w3.org/WAI/WCAG21/Understanding/audio-control.html",
|
|
33
|
+
"description": "Media element autoplays without user control (WCAG 1.4.2, 2.2.2)",
|
|
34
|
+
"failureMessage": "Media element has autoplay attribute without providing user controls to stop it",
|
|
35
|
+
"axeEquivalents": []
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "cdp-missing-main-landmark",
|
|
39
|
+
"condition": "dom-eval",
|
|
40
|
+
"impact": "moderate",
|
|
41
|
+
"tags": ["wcag2a","wcag131","best-practice","cdp-check"],
|
|
42
|
+
"help": "Page must have a main landmark",
|
|
43
|
+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/landmark-one-main",
|
|
44
|
+
"description": "Page does not have a main landmark",
|
|
45
|
+
"failureMessage": "Document does not contain a <main> element or an element with role=\"main\"",
|
|
46
|
+
"axeEquivalents": ["landmark-one-main"]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "cdp-missing-skip-link",
|
|
50
|
+
"condition": "dom-eval",
|
|
51
|
+
"impact": "moderate",
|
|
52
|
+
"tags": ["wcag2a","wcag241","best-practice","cdp-check"],
|
|
53
|
+
"help": "Page must have a skip navigation link",
|
|
54
|
+
"helpUrl": "https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html",
|
|
55
|
+
"description": "Page does not have a skip navigation link as the first focusable element",
|
|
56
|
+
"failureMessage": "No skip link found as first focusable element — keyboard users cannot bypass navigation",
|
|
57
|
+
"axeEquivalents": ["bypass"]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
package/docs/api-reference.md
CHANGED
|
@@ -106,7 +106,7 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
106
106
|
| `viewport` | `object` | `{ width: 1280, height: 800 }` | width: 320–2560, height: 320–2560 | Browser viewport dimensions in pixels |
|
|
107
107
|
| `colorScheme` | `string` | `"light"` | `"light"` \| `"dark"` | Emulates `prefers-color-scheme` media query |
|
|
108
108
|
| `engines` | `object` | all `true` | `{ axe?, cdp?, pa11y? }` | Which engines to run. At least one must be enabled |
|
|
109
|
-
| `axeTags` | `string[]` | WCAG 2.x A+AA | See below | axe-core rule tag filter. Also determines pa11y standard |
|
|
109
|
+
| `axeTags` | `string[]` | WCAG 2.x A+AA | See below | axe-core rule tag filter. Also determines pa11y standard. Add `"best-practice"` and/or `"ACT"` to include non-WCAG best practices and W3C ACT rules (opt-in, not included by default) |
|
|
110
110
|
| `onlyRule` | `string` | — | axe rule ID | Run a single axe rule only (e.g. `"color-contrast"`) |
|
|
111
111
|
| `ignoreFindings` | `string[]` | — | axe rule IDs | Suppress specific rules from output entirely |
|
|
112
112
|
| `excludeSelectors` | `string[]` | — | CSS selectors | Skip elements matching these selectors during axe scan |
|
|
@@ -121,6 +121,8 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
121
121
|
| `ai.githubToken` | `string` | — | GitHub PAT | Used to fetch source files from the repo for AI context |
|
|
122
122
|
| `ai.model` | `string` | `"claude-haiku-4-5-20251001"` | Anthropic model ID | Claude model to use |
|
|
123
123
|
| `ai.systemPrompt` | `string` | Built-in prompt | — | Overrides the default Claude system prompt for the entire scan |
|
|
124
|
+
| `clearCache` | `boolean` | `false` | — | Clear browser cache before each page navigation via CDP `Network.clearBrowserCache`. Ensures fresh results on repeated scans of the same domain |
|
|
125
|
+
| `serverMode` | `boolean` | `false` | — | Enable server/EC2/Docker Chrome launch flags: `--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`, `--disable-gpu`, `--no-zygote`, `--disable-accelerated-2d-canvas`. Use in CI, Docker, or EC2 environments |
|
|
124
126
|
| `onProgress` | `function` | — | — | Callback fired at each pipeline step |
|
|
125
127
|
|
|
126
128
|
**`axeTags` common values:**
|
|
@@ -134,10 +136,25 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
134
136
|
| `wcag22a` | WCAG 2.2 Level A additions |
|
|
135
137
|
| `wcag22aa` | WCAG 2.2 Level AA additions |
|
|
136
138
|
| `wcag2aaa` | WCAG 2.0 Level AAA |
|
|
137
|
-
| `best-practice` | Non-WCAG best practices |
|
|
139
|
+
| `best-practice` | Non-WCAG best practices (opt-in) |
|
|
140
|
+
| `ACT` | W3C Accessibility Conformance Testing rules (opt-in) |
|
|
138
141
|
|
|
139
142
|
**Supported `framework` values:** `nextjs`, `gatsby`, `react`, `nuxt`, `vue`, `angular`, `astro`, `svelte`, `remix`, `shopify`, `wordpress`, `drupal`
|
|
140
143
|
|
|
144
|
+
**CDP checks:**
|
|
145
|
+
|
|
146
|
+
The `cdp` engine runs 5 checks split across two mechanisms:
|
|
147
|
+
|
|
148
|
+
| Check ID | Mechanism | Impact | WCAG |
|
|
149
|
+
| :--- | :--- | :--- | :--- |
|
|
150
|
+
| `cdp-missing-accessible-name` | Accessibility tree | Serious | 4.1.2 A |
|
|
151
|
+
| `cdp-aria-hidden-focusable` | Accessibility tree | Serious | 4.1.2 A |
|
|
152
|
+
| `cdp-autoplay-media` | `page.evaluate()` | Serious | 1.4.2, 2.2.2 A |
|
|
153
|
+
| `cdp-missing-main-landmark` | `page.evaluate()` | Moderate | 1.3.1 A |
|
|
154
|
+
| `cdp-missing-skip-link` | `page.evaluate()` | Moderate | 2.4.1 A |
|
|
155
|
+
|
|
156
|
+
All 5 checks have intelligence enrichment entries (fix description, fix code, framework notes, CMS notes).
|
|
157
|
+
|
|
141
158
|
**`onProgress` callback:**
|
|
142
159
|
|
|
143
160
|
```ts
|
|
@@ -182,6 +199,9 @@ Returns: `Promise<ScanPayload>`
|
|
|
182
199
|
},
|
|
183
200
|
routes_scanned: number, // How many pages were actually scanned
|
|
184
201
|
discovery_method: string, // "crawl" | "explicit"
|
|
202
|
+
passesCount: number, // Unique axe rules that passed (deduplicated across routes)
|
|
203
|
+
incompleteCount: number, // Total axe incomplete results across routes (needs manual review)
|
|
204
|
+
inapplicableCount: number, // Unique axe rules that were inapplicable (deduplicated across routes)
|
|
185
205
|
},
|
|
186
206
|
incomplete_findings?: RawFinding[], // axe "incomplete" results (needs-review)
|
|
187
207
|
patternFindings?: { // Only present if projectDir/repoUrl + !skipPatterns
|
package/docs/architecture.md
CHANGED
|
@@ -82,7 +82,7 @@ flowchart LR
|
|
|
82
82
|
|
|
83
83
|
| Module | Responsibility |
|
|
84
84
|
| :--- | :--- |
|
|
85
|
-
| `src/pipeline/dom-scanner.mjs` | Route discovery, engine execution (axe/CDP/pa11y), merge/dedup, progress updates, screenshots |
|
|
85
|
+
| `src/pipeline/dom-scanner.mjs` | Route discovery, engine execution (axe/CDP/pa11y), merge/dedup, progress updates, screenshots. CDP checks use two mechanisms: CDP accessibility tree (missing accessible name, aria-hidden focusable) and `page.evaluate()` DOM inspection (autoplay media, missing main landmark, missing skip link). |
|
|
86
86
|
| `src/enrichment/analyzer.mjs` | Rule enrichment, selector strategy, ownership hints, recommendations, scoring metadata |
|
|
87
87
|
| `src/ai/enrich.mjs` | CLI subprocess that runs AI enrichment after the analyzer. Reads `ANTHROPIC_API_KEY` and `AI_SYSTEM_PROMPT` env vars. Non-fatal. |
|
|
88
88
|
| `src/ai/claude.mjs` | Anthropic API client. Sends Critical/Serious findings to Claude and parses improved fix suggestions. Supports custom system prompt and repo source file context. |
|
|
@@ -120,6 +120,29 @@ Output shape:
|
|
|
120
120
|
Core artifacts generated by the pipeline:
|
|
121
121
|
|
|
122
122
|
- `progress.json`: step status — `page`, `axe`, `cdp`, `pa11y`, `merge`, `intelligence`, `repo` (remote package.json fetch), `patterns` (source pattern scan), `ai` (Claude enrichment)
|
|
123
|
+
|
|
124
|
+
### CDP Check Mechanisms
|
|
125
|
+
|
|
126
|
+
The CDP engine uses two distinct detection mechanisms within the same `runCdpChecks()` function:
|
|
127
|
+
|
|
128
|
+
| Mechanism | Checks | Why |
|
|
129
|
+
| :--- | :--- | :--- |
|
|
130
|
+
| CDP Accessibility Tree (`Accessibility.getFullAXTree`) | `cdp-missing-accessible-name`, `cdp-aria-hidden-focusable` | Reads the computed accessibility tree — the actual view screen readers see. Required for role/name/state inspection. |
|
|
131
|
+
| DOM evaluation (`page.evaluate()`) | `cdp-autoplay-media`, `cdp-missing-main-landmark`, `cdp-missing-skip-link` | Inspects HTML attributes and DOM structure that are not reflected in the accessibility tree. `autoplay` is a media attribute; landmark and skip link presence are structural checks. |
|
|
132
|
+
|
|
133
|
+
### Engine Execution Model
|
|
134
|
+
|
|
135
|
+
Within each route, the three engines run with optimized parallelism:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
┌─ axe-core (navigates + analyzes page) ──────────────────────────────┐
|
|
139
|
+
│ └─ CDP checks (reads same loaded page — sequential after axe) ├──→ merge
|
|
140
|
+
└─ pa11y (own shared Puppeteer browser, independent) ─────────────────┘
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **axe → CDP**: Sequential. CDP depends on axe's page navigation. Both share the same Playwright tab.
|
|
144
|
+
- **pa11y**: Starts in parallel with axe. Uses a single shared Puppeteer browser instance (launched once per scan, reused for all routes). This eliminates the Chrome cold-start overhead (1-3s) that would occur if pa11y launched its own browser per route.
|
|
145
|
+
- **Route batching**: Up to 3 routes are scanned in parallel via Playwright tabs. Each route runs its own axe→CDP→(merge with pa11y) sequence.
|
|
123
146
|
- `a11y-scan-results.json`: merged runtime scan output per route
|
|
124
147
|
- `a11y-findings.json`: enriched findings payload used by reports and API consumers
|
|
125
148
|
|
package/docs/intelligence.md
CHANGED
|
@@ -51,7 +51,7 @@ flowchart TD
|
|
|
51
51
|
|
|
52
52
|
## Intelligence Database
|
|
53
53
|
|
|
54
|
-
The engine ships a bundled intelligence database at `assets/remediation/intelligence.mjs`. This contains per-rule entries keyed by axe rule ID (e.g. `color-contrast`, `image-alt`).
|
|
54
|
+
The engine ships a bundled intelligence database at `assets/remediation/intelligence.mjs`. This contains per-rule entries keyed by axe rule ID (e.g. `color-contrast`, `image-alt`) and CDP check IDs (e.g. `cdp-autoplay-media`, `cdp-missing-main-landmark`, `cdp-missing-skip-link`).
|
|
55
55
|
|
|
56
56
|
Each rule entry can include:
|
|
57
57
|
|
|
@@ -254,7 +254,7 @@ The default system prompt instructs Claude to go beyond the generic fix: explain
|
|
|
254
254
|
|
|
255
255
|
| Asset | Used by | Purpose |
|
|
256
256
|
| :--- | :--- | :--- |
|
|
257
|
-
| `remediation/intelligence.mjs` | analyzer | Per-rule fix descriptions, code, guardrails, framework notes |
|
|
257
|
+
| `remediation/intelligence.mjs` | analyzer | Per-rule fix descriptions, code, guardrails, framework notes. Covers 101 axe-core rules + 3 CDP-specific checks (`cdp-autoplay-media`, `cdp-missing-main-landmark`, `cdp-missing-skip-link`) |
|
|
258
258
|
| `remediation/axe-check-maps.mjs` | analyzer | Failure mode and relationship hint mappings from axe check IDs |
|
|
259
259
|
| `remediation/guardrails.mjs` | analyzer | Shared guardrail constraints for safe automated fixes |
|
|
260
260
|
| `remediation/source-boundaries.mjs` | analyzer, source-scanner | Framework-specific source directory scoping |
|
package/docs/outputs.md
CHANGED
|
@@ -89,7 +89,8 @@ Merged results from all three engines (axe-core + CDP + pa11y) per route. Writte
|
|
|
89
89
|
"url": "https://example.com/",
|
|
90
90
|
"violations": [...],
|
|
91
91
|
"incomplete": [...],
|
|
92
|
-
"passes": [...]
|
|
92
|
+
"passes": ["aria-hidden-focus", "document-title", ...],
|
|
93
|
+
"inapplicable": ["video-caption", "audio-caption", ...]
|
|
93
94
|
}
|
|
94
95
|
]
|
|
95
96
|
}
|
|
@@ -128,6 +129,9 @@ The primary enriched data artifact. Written by `src/enrichment/analyzer.mjs`. Th
|
|
|
128
129
|
| `recommendations` | `object[]` | Grouped fix recommendations by component |
|
|
129
130
|
| `testingMethodology` | `object` | Scan scope and methodology summary |
|
|
130
131
|
| `fpFiltered` | `number` | Count of findings filtered as likely false positives |
|
|
132
|
+
| `passesCount` | `number` | Unique axe-core rules that passed (deduplicated across all scanned routes) |
|
|
133
|
+
| `incompleteCount` | `number` | Total axe-core incomplete results across all routes — items that need manual review |
|
|
134
|
+
| `inapplicableCount` | `number` | Unique axe-core rules that were inapplicable (deduplicated across all scanned routes) |
|
|
131
135
|
| `deduplicatedCount` | `number` | Count of duplicate findings removed |
|
|
132
136
|
|
|
133
137
|
### `findings` — per-finding fields
|
package/package.json
CHANGED
package/src/ai/claude.mjs
CHANGED
|
@@ -41,6 +41,21 @@ Rules:
|
|
|
41
41
|
- If the violation data contains specific values (colors, ratios, labels), use them in your response
|
|
42
42
|
- Respond in JSON only — no markdown, no explanation outside the JSON structure`;
|
|
43
43
|
|
|
44
|
+
export const PM_AI_SYSTEM_PROMPT = `You are an accessibility compliance advisor for product managers and non-technical stakeholders.
|
|
45
|
+
|
|
46
|
+
Your task is to provide a business-oriented summary for each accessibility finding — something a PM can use to prioritize, communicate to stakeholders, and plan sprints.
|
|
47
|
+
|
|
48
|
+
For each finding, provide:
|
|
49
|
+
1. pmSummary: A single sentence describing who is affected and what they cannot do. Use plain language, no technical jargon.
|
|
50
|
+
2. pmImpact: 2-3 sentences on business consequences: legal/compliance risk, user segments blocked, effect on conversions/engagement/SEO. Be specific to the violation.
|
|
51
|
+
3. pmEffort: One of "quick-win", "medium", or "strategic" with a brief time estimate (e.g., "quick-win — under 1 hour per instance").
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
- Write for a non-technical audience — no code, no selectors, no ARIA terminology
|
|
55
|
+
- Focus on users affected, business risk, and prioritization
|
|
56
|
+
- Reference the actual violation data (title, severity, affected users) to be specific
|
|
57
|
+
- Respond in JSON only — no markdown, no explanation outside the JSON structure`;
|
|
58
|
+
|
|
44
59
|
function buildSystemPrompt(context) {
|
|
45
60
|
const { framework, cms, uiLibraries } = context.stack || {};
|
|
46
61
|
|
|
@@ -98,6 +113,37 @@ function buildUserMessage(findings, sourceFiles) {
|
|
|
98
113
|
return message;
|
|
99
114
|
}
|
|
100
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Builds the user message for PM audience.
|
|
118
|
+
* @param {EnrichedFinding[]} findings
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function buildPmUserMessage(findings) {
|
|
122
|
+
const items = findings.map((f, i) => ({
|
|
123
|
+
index: i,
|
|
124
|
+
title: f.title,
|
|
125
|
+
severity: f.severity,
|
|
126
|
+
wcag: f.wcag,
|
|
127
|
+
impactedUsers: f.impactedUsers,
|
|
128
|
+
currentPmSummary: f.pmSummary,
|
|
129
|
+
currentPmImpact: f.pmImpact,
|
|
130
|
+
totalInstances: f.totalInstances,
|
|
131
|
+
pagesAffected: f.pagesAffected,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
let message = `Improve the PM-facing guidance for these ${findings.length} accessibility finding(s).\n\n`;
|
|
135
|
+
message += `FINDINGS:\n${JSON.stringify(items, null, 2)}\n`;
|
|
136
|
+
message += `\nRespond with a JSON array where each item has:\n`;
|
|
137
|
+
message += `{
|
|
138
|
+
"index": <number>,
|
|
139
|
+
"pmSummary": "<one-line business impact>",
|
|
140
|
+
"pmImpact": "<2-3 sentences on business/legal/UX consequences>",
|
|
141
|
+
"pmEffort": "<quick-win|medium|strategic with time estimate>"
|
|
142
|
+
}`;
|
|
143
|
+
|
|
144
|
+
return message;
|
|
145
|
+
}
|
|
146
|
+
|
|
101
147
|
/**
|
|
102
148
|
* Calls the Claude API with the given messages.
|
|
103
149
|
* @param {string} apiKey
|
|
@@ -266,6 +312,7 @@ export async function enrichWithAI(findings, context = {}, options = {}) {
|
|
|
266
312
|
if (!enabled) return findings;
|
|
267
313
|
|
|
268
314
|
const model = options.model || DEFAULT_MODEL;
|
|
315
|
+
const audience = options.audience || "dev";
|
|
269
316
|
const customSystemPrompt = options.systemPrompt || null;
|
|
270
317
|
|
|
271
318
|
// Only enrich Critical and Serious findings, cap total
|
|
@@ -281,11 +328,16 @@ export async function enrichWithAI(findings, context = {}, options = {}) {
|
|
|
281
328
|
? await fetchSourceFilesForFindings(targets, context.repoUrl, options.githubToken)
|
|
282
329
|
: {};
|
|
283
330
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
const isPm = audience === "pm";
|
|
332
|
+
const systemPrompt = customSystemPrompt || (isPm
|
|
333
|
+
? PM_AI_SYSTEM_PROMPT
|
|
334
|
+
: buildSystemPrompt({
|
|
335
|
+
stack: context.stack,
|
|
336
|
+
hasSourceCode: Object.keys(sourceFiles).length > 0,
|
|
337
|
+
}));
|
|
338
|
+
const userMessage = isPm
|
|
339
|
+
? buildPmUserMessage(targets)
|
|
340
|
+
: buildUserMessage(targets, sourceFiles);
|
|
289
341
|
|
|
290
342
|
const responseText = await callClaude(options.apiKey, model, systemPrompt, userMessage);
|
|
291
343
|
|
|
@@ -311,6 +363,15 @@ export async function enrichWithAI(findings, context = {}, options = {}) {
|
|
|
311
363
|
const imp = improvementMap.get(targetIdx);
|
|
312
364
|
if (!imp) return finding;
|
|
313
365
|
|
|
366
|
+
if (isPm) {
|
|
367
|
+
return {
|
|
368
|
+
...finding,
|
|
369
|
+
pmSummary: imp.pmSummary || finding.pmSummary,
|
|
370
|
+
pmImpact: imp.pmImpact || finding.pmImpact,
|
|
371
|
+
pmEffort: imp.pmEffort || finding.pmEffort,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
314
375
|
return {
|
|
315
376
|
...finding,
|
|
316
377
|
fixDescription: imp.fixDescription || finding.fixDescription,
|
|
@@ -639,6 +639,29 @@ function computeOverallAssessment(findings) {
|
|
|
639
639
|
return "Pass";
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Computes pass/incomplete/inapplicable counts across all scanned routes.
|
|
644
|
+
* @param {Object[]} routes - Raw scan routes.
|
|
645
|
+
* @returns {{ passesCount: number, incompleteCount: number, inapplicableCount: number }}
|
|
646
|
+
*/
|
|
647
|
+
function computeAxeCounts(routes) {
|
|
648
|
+
const passedRules = new Set();
|
|
649
|
+
const inapplicableRules = new Set();
|
|
650
|
+
let incompleteCount = 0;
|
|
651
|
+
|
|
652
|
+
for (const route of routes) {
|
|
653
|
+
for (const ruleId of route.passes || []) passedRules.add(ruleId);
|
|
654
|
+
for (const ruleId of route.inapplicable || []) inapplicableRules.add(ruleId);
|
|
655
|
+
incompleteCount += (route.incomplete || []).length;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
passesCount: passedRules.size,
|
|
660
|
+
incompleteCount,
|
|
661
|
+
inapplicableCount: inapplicableRules.size,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
642
665
|
/**
|
|
643
666
|
* Aggregates WCAG 2.2 AA criteria that passed across all scanned routes.
|
|
644
667
|
* @param {Object[]} routes - Raw scan routes with a passes array of rule IDs.
|
|
@@ -907,6 +930,9 @@ function buildFindings(inputPayload, cliArgs) {
|
|
|
907
930
|
needs_verification: !!v._fromIncomplete,
|
|
908
931
|
verification_command: `pnpm a11y --base-url ${route.url} --routes ${route.path} --only-rule ${v.id} --max-routes 1`,
|
|
909
932
|
verification_command_fallback: `node scripts/audit.mjs --base-url ${route.url} --routes ${route.path} --only-rule ${v.id} --max-routes 1`,
|
|
933
|
+
pm_summary: ruleInfo.pm?.summary ?? null,
|
|
934
|
+
pm_impact: ruleInfo.pm?.impact ?? null,
|
|
935
|
+
pm_effort: ruleInfo.pm?.effort ?? null,
|
|
910
936
|
});
|
|
911
937
|
}
|
|
912
938
|
}
|
|
@@ -1044,6 +1070,7 @@ export function runAnalyzer(scanPayload, options = {}) {
|
|
|
1044
1070
|
const recommendations = computeRecommendations(dedupedFindings);
|
|
1045
1071
|
const testingMethodology = computeTestingMethodology(scanPayload);
|
|
1046
1072
|
const incompleteFindings = collectIncompleteFindings(scanPayload.routes || []);
|
|
1073
|
+
const axeCounts = computeAxeCounts(scanPayload.routes || []);
|
|
1047
1074
|
if (incompleteFindings.length > 0)
|
|
1048
1075
|
log.info(`${incompleteFindings.length} incomplete finding(s) require manual review.`);
|
|
1049
1076
|
|
|
@@ -1060,6 +1087,9 @@ export function runAnalyzer(scanPayload, options = {}) {
|
|
|
1060
1087
|
testingMethodology,
|
|
1061
1088
|
fpFiltered: fpRemovedCount,
|
|
1062
1089
|
deduplicatedCount,
|
|
1090
|
+
passesCount: axeCounts.passesCount,
|
|
1091
|
+
incompleteCount: axeCounts.incompleteCount,
|
|
1092
|
+
inapplicableCount: axeCounts.inapplicableCount,
|
|
1063
1093
|
},
|
|
1064
1094
|
};
|
|
1065
1095
|
|
package/src/index.d.mts
CHANGED
|
@@ -52,6 +52,9 @@ export interface Finding {
|
|
|
52
52
|
pages_affected?: number | null;
|
|
53
53
|
affected_urls?: string[] | null;
|
|
54
54
|
needs_verification?: boolean;
|
|
55
|
+
pm_summary: string | null;
|
|
56
|
+
pm_impact: string | null;
|
|
57
|
+
pm_effort: string | null;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface EnrichedFinding {
|
|
@@ -104,6 +107,9 @@ export interface EnrichedFinding {
|
|
|
104
107
|
pagesAffected: number | null;
|
|
105
108
|
affectedUrls: string[] | null;
|
|
106
109
|
needsVerification?: boolean;
|
|
110
|
+
pmSummary: string | null;
|
|
111
|
+
pmImpact: string | null;
|
|
112
|
+
pmEffort: string | null;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
export interface SeverityTotals {
|
|
@@ -141,9 +147,32 @@ export interface AuditSummary {
|
|
|
141
147
|
|
|
142
148
|
|
|
143
149
|
|
|
150
|
+
export interface ScanMetadata {
|
|
151
|
+
target_url?: string;
|
|
152
|
+
scanned_at?: string;
|
|
153
|
+
engines?: { axe: boolean; cdp: boolean; pa11y: boolean };
|
|
154
|
+
projectContext?: { framework: string | null; cms: string | null; uiLibraries: string[] };
|
|
155
|
+
routes_scanned?: number;
|
|
156
|
+
discovery_method?: string;
|
|
157
|
+
overallAssessment?: Record<string, unknown>;
|
|
158
|
+
passedCriteria?: string[];
|
|
159
|
+
outOfScope?: Record<string, unknown>;
|
|
160
|
+
recommendations?: unknown[];
|
|
161
|
+
testingMethodology?: Record<string, unknown>;
|
|
162
|
+
fpFiltered?: number;
|
|
163
|
+
deduplicatedCount?: number;
|
|
164
|
+
/** Total number of unique axe-core rules that passed (deduplicated across all scanned routes). */
|
|
165
|
+
passesCount?: number;
|
|
166
|
+
/** Total number of axe-core incomplete results across all scanned routes (needs manual review). */
|
|
167
|
+
incompleteCount?: number;
|
|
168
|
+
/** Total number of unique axe-core rules that were inapplicable (deduplicated across all scanned routes). */
|
|
169
|
+
inapplicableCount?: number;
|
|
170
|
+
[key: string]: unknown;
|
|
171
|
+
}
|
|
172
|
+
|
|
144
173
|
export interface ScanPayload {
|
|
145
174
|
findings: Finding[] | Record<string, unknown>[];
|
|
146
|
-
metadata?:
|
|
175
|
+
metadata?: ScanMetadata;
|
|
147
176
|
incomplete_findings?: unknown[];
|
|
148
177
|
}
|
|
149
178
|
|
|
@@ -391,7 +420,31 @@ export interface RunAuditOptions {
|
|
|
391
420
|
waitUntil?: string;
|
|
392
421
|
colorScheme?: string;
|
|
393
422
|
viewport?: { width: number; height: number };
|
|
423
|
+
/**
|
|
424
|
+
* axe-core rule tag filter. Also determines the pa11y standard used.
|
|
425
|
+
* Default: `["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22a","wcag22aa"]`
|
|
426
|
+
*
|
|
427
|
+
* Optional opt-in tags (not included by default):
|
|
428
|
+
* - `"best-practice"` — non-WCAG best practices (duplicate IDs, landmark structure, etc.)
|
|
429
|
+
* - `"ACT"` — W3C Accessibility Conformance Testing rules
|
|
430
|
+
* - `"wcag2aaa"` / `"wcag21aaa"` — WCAG Level AAA rules
|
|
431
|
+
*/
|
|
394
432
|
axeTags?: string[];
|
|
433
|
+
/**
|
|
434
|
+
* Clear browser cache before each page navigation.
|
|
435
|
+
* Ensures fresh scan results when scanning the same domain multiple times.
|
|
436
|
+
* Uses CDP `Network.clearBrowserCache` + `Network.setCacheDisabled`.
|
|
437
|
+
* Default: `false`
|
|
438
|
+
*/
|
|
439
|
+
clearCache?: boolean;
|
|
440
|
+
/**
|
|
441
|
+
* Enable server/EC2/Docker-optimized Chrome launch flags.
|
|
442
|
+
* Adds: `--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`,
|
|
443
|
+
* `--disable-gpu`, `--no-zygote`, `--disable-accelerated-2d-canvas`.
|
|
444
|
+
* Use this when running in CI, Docker, or EC2 environments.
|
|
445
|
+
* Default: `false`
|
|
446
|
+
*/
|
|
447
|
+
serverMode?: boolean;
|
|
395
448
|
onlyRule?: string;
|
|
396
449
|
excludeSelectors?: string[];
|
|
397
450
|
ignoreFindings?: string[];
|
|
@@ -426,7 +479,8 @@ export function getFindings(
|
|
|
426
479
|
|
|
427
480
|
export function getOverview(
|
|
428
481
|
findings: EnrichedFinding[],
|
|
429
|
-
payload?: ScanPayload | null
|
|
482
|
+
payload?: ScanPayload | null,
|
|
483
|
+
options?: { countIncompleteInScore?: boolean }
|
|
430
484
|
): AuditSummary;
|
|
431
485
|
|
|
432
486
|
export function getPDFReport(
|
|
@@ -456,6 +510,7 @@ export function getSourcePatterns(
|
|
|
456
510
|
export function getKnowledge(options?: KnowledgeOptions): EngineKnowledge;
|
|
457
511
|
|
|
458
512
|
export const DEFAULT_AI_SYSTEM_PROMPT: string;
|
|
513
|
+
export const PM_AI_SYSTEM_PROMPT: string;
|
|
459
514
|
|
|
460
515
|
export interface ViewportPreset {
|
|
461
516
|
label: string;
|
|
@@ -471,4 +526,5 @@ export interface AiOptions {
|
|
|
471
526
|
githubToken?: string;
|
|
472
527
|
model?: string;
|
|
473
528
|
systemPrompt?: string;
|
|
529
|
+
audience?: "pm" | "dev";
|
|
474
530
|
}
|
package/src/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ASSET_PATHS, loadAssetJson } from "./core/asset-loader.mjs";
|
|
8
|
-
export { DEFAULT_AI_SYSTEM_PROMPT } from "./ai/claude.mjs";
|
|
8
|
+
export { DEFAULT_AI_SYSTEM_PROMPT, PM_AI_SYSTEM_PROMPT } from "./ai/claude.mjs";
|
|
9
9
|
|
|
10
10
|
// Lazy-loaded asset cache
|
|
11
11
|
|
|
@@ -162,6 +162,9 @@ function normalizeSingleFinding(item, index, screenshotUrlBuilder) {
|
|
|
162
162
|
pages_affected: typeof item.pages_affected === "number" ? item.pages_affected : null,
|
|
163
163
|
affected_urls: Array.isArray(item.affected_urls) ? item.affected_urls : null,
|
|
164
164
|
needs_verification: Boolean(item.needs_verification),
|
|
165
|
+
pm_summary: strOrNull(item.pm_summary),
|
|
166
|
+
pm_impact: strOrNull(item.pm_impact),
|
|
167
|
+
pm_effort: strOrNull(item.pm_effort),
|
|
165
168
|
};
|
|
166
169
|
}
|
|
167
170
|
|
|
@@ -249,6 +252,9 @@ export function getFindings(input, options = {}) {
|
|
|
249
252
|
pagesAffected: finding.pages_affected,
|
|
250
253
|
affectedUrls: finding.affected_urls,
|
|
251
254
|
needsVerification: finding.needs_verification,
|
|
255
|
+
pmSummary: finding.pm_summary ?? null,
|
|
256
|
+
pmImpact: finding.pm_impact ?? null,
|
|
257
|
+
pmEffort: finding.pm_effort ?? null,
|
|
252
258
|
};
|
|
253
259
|
|
|
254
260
|
// Enrich from intelligence if no fix data exists yet
|
|
@@ -378,10 +384,13 @@ function getPersonaGroups(findings) {
|
|
|
378
384
|
*
|
|
379
385
|
* @param {object[]} findings - Array of enriched findings.
|
|
380
386
|
* @param {{ findings: object[], metadata?: object }|null} [payload=null] - Original scan payload for metadata extraction.
|
|
387
|
+
* @param {{ countIncompleteInScore?: boolean }} [options={}] - Scoring options.
|
|
381
388
|
* @returns {object} Full audit summary.
|
|
382
389
|
*/
|
|
383
|
-
export function getOverview(findings, payload = null) {
|
|
384
|
-
const scorableFindings =
|
|
390
|
+
export function getOverview(findings, payload = null, options = {}) {
|
|
391
|
+
const scorableFindings = options.countIncompleteInScore
|
|
392
|
+
? findings
|
|
393
|
+
: findings.filter((f) => !f.needsVerification);
|
|
385
394
|
const totals = { Critical: 0, Serious: 0, Moderate: 0, Minor: 0 };
|
|
386
395
|
for (const f of scorableFindings) {
|
|
387
396
|
const severity = f.severity || "";
|
|
@@ -685,6 +694,8 @@ export async function runAudit(options) {
|
|
|
685
694
|
projectDir: options.projectDir,
|
|
686
695
|
remotePackageJson,
|
|
687
696
|
engines,
|
|
697
|
+
clearCache: options.clearCache ?? false,
|
|
698
|
+
serverMode: options.serverMode ?? false,
|
|
688
699
|
},
|
|
689
700
|
{ onProgress },
|
|
690
701
|
);
|
|
@@ -790,6 +801,7 @@ export async function runAudit(options) {
|
|
|
790
801
|
apiKey: aiOptions.apiKey,
|
|
791
802
|
githubToken: aiOptions.githubToken || options.githubToken,
|
|
792
803
|
model: aiOptions.model,
|
|
804
|
+
audience: aiOptions.audience || "dev",
|
|
793
805
|
}
|
|
794
806
|
);
|
|
795
807
|
|