@diegovelasquezweb/a11y-engine 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +42 -2
- package/docs/architecture.md +24 -1
- package/docs/intelligence.md +15 -2
- package/docs/outputs.md +8 -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 +56 -1
- package/src/index.mjs +10 -1
- 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
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- [Constants](#constants)
|
|
25
25
|
- [VIEWPORT_PRESETS](#viewport_presets)
|
|
26
26
|
- [DEFAULT_AI_SYSTEM_PROMPT](#default_ai_system_prompt)
|
|
27
|
+
- [PM_AI_SYSTEM_PROMPT](#pm_ai_system_prompt)
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
getKnowledge,
|
|
53
54
|
VIEWPORT_PRESETS,
|
|
54
55
|
DEFAULT_AI_SYSTEM_PROMPT,
|
|
56
|
+
PM_AI_SYSTEM_PROMPT,
|
|
55
57
|
} from "@diegovelasquezweb/a11y-engine";
|
|
56
58
|
```
|
|
57
59
|
|
|
@@ -106,7 +108,7 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
106
108
|
| `viewport` | `object` | `{ width: 1280, height: 800 }` | width: 320–2560, height: 320–2560 | Browser viewport dimensions in pixels |
|
|
107
109
|
| `colorScheme` | `string` | `"light"` | `"light"` \| `"dark"` | Emulates `prefers-color-scheme` media query |
|
|
108
110
|
| `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 |
|
|
111
|
+
| `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
112
|
| `onlyRule` | `string` | — | axe rule ID | Run a single axe rule only (e.g. `"color-contrast"`) |
|
|
111
113
|
| `ignoreFindings` | `string[]` | — | axe rule IDs | Suppress specific rules from output entirely |
|
|
112
114
|
| `excludeSelectors` | `string[]` | — | CSS selectors | Skip elements matching these selectors during axe scan |
|
|
@@ -121,6 +123,9 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
121
123
|
| `ai.githubToken` | `string` | — | GitHub PAT | Used to fetch source files from the repo for AI context |
|
|
122
124
|
| `ai.model` | `string` | `"claude-haiku-4-5-20251001"` | Anthropic model ID | Claude model to use |
|
|
123
125
|
| `ai.systemPrompt` | `string` | Built-in prompt | — | Overrides the default Claude system prompt for the entire scan |
|
|
126
|
+
| `ai.audience` | `string` | `"dev"` | `"dev"` \| `"pm"` | Controls the AI enrichment tone. `"dev"` generates code-level fixes; `"pm"` generates business impact summaries |
|
|
127
|
+
| `clearCache` | `boolean` | `false` | — | Clear browser cache before each page navigation via CDP `Network.clearBrowserCache`. Ensures fresh results on repeated scans of the same domain |
|
|
128
|
+
| `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
129
|
| `onProgress` | `function` | — | — | Callback fired at each pipeline step |
|
|
125
130
|
|
|
126
131
|
**`axeTags` common values:**
|
|
@@ -134,10 +139,25 @@ Returns a `ScanPayload` object consumed by all other functions.
|
|
|
134
139
|
| `wcag22a` | WCAG 2.2 Level A additions |
|
|
135
140
|
| `wcag22aa` | WCAG 2.2 Level AA additions |
|
|
136
141
|
| `wcag2aaa` | WCAG 2.0 Level AAA |
|
|
137
|
-
| `best-practice` | Non-WCAG best practices |
|
|
142
|
+
| `best-practice` | Non-WCAG best practices (opt-in) |
|
|
143
|
+
| `ACT` | W3C Accessibility Conformance Testing rules (opt-in) |
|
|
138
144
|
|
|
139
145
|
**Supported `framework` values:** `nextjs`, `gatsby`, `react`, `nuxt`, `vue`, `angular`, `astro`, `svelte`, `remix`, `shopify`, `wordpress`, `drupal`
|
|
140
146
|
|
|
147
|
+
**CDP checks:**
|
|
148
|
+
|
|
149
|
+
The `cdp` engine runs 5 checks split across two mechanisms:
|
|
150
|
+
|
|
151
|
+
| Check ID | Mechanism | Impact | WCAG |
|
|
152
|
+
| :--- | :--- | :--- | :--- |
|
|
153
|
+
| `cdp-missing-accessible-name` | Accessibility tree | Serious | 4.1.2 A |
|
|
154
|
+
| `cdp-aria-hidden-focusable` | Accessibility tree | Serious | 4.1.2 A |
|
|
155
|
+
| `cdp-autoplay-media` | `page.evaluate()` | Serious | 1.4.2, 2.2.2 A |
|
|
156
|
+
| `cdp-missing-main-landmark` | `page.evaluate()` | Moderate | 1.3.1 A |
|
|
157
|
+
| `cdp-missing-skip-link` | `page.evaluate()` | Moderate | 2.4.1 A |
|
|
158
|
+
|
|
159
|
+
All 5 checks have intelligence enrichment entries (fix description, fix code, framework notes, CMS notes).
|
|
160
|
+
|
|
141
161
|
**`onProgress` callback:**
|
|
142
162
|
|
|
143
163
|
```ts
|
|
@@ -182,6 +202,9 @@ Returns: `Promise<ScanPayload>`
|
|
|
182
202
|
},
|
|
183
203
|
routes_scanned: number, // How many pages were actually scanned
|
|
184
204
|
discovery_method: string, // "crawl" | "explicit"
|
|
205
|
+
passesCount: number, // Unique axe rules that passed (deduplicated across routes)
|
|
206
|
+
incompleteCount: number, // Total axe incomplete results across routes (needs manual review)
|
|
207
|
+
inapplicableCount: number, // Unique axe rules that were inapplicable (deduplicated across routes)
|
|
185
208
|
},
|
|
186
209
|
incomplete_findings?: RawFinding[], // axe "incomplete" results (needs-review)
|
|
187
210
|
patternFindings?: { // Only present if projectDir/repoUrl + !skipPatterns
|
|
@@ -308,6 +331,11 @@ Returns: `EnrichedFinding[]`
|
|
|
308
331
|
guardrails: object | null, // Guardrail metadata from the engine
|
|
309
332
|
checkData: object | null, // Raw check data from the engine
|
|
310
333
|
|
|
334
|
+
// PM audience fields (always present from intelligence DB)
|
|
335
|
+
pmSummary: string | null, // One-line business impact for PMs
|
|
336
|
+
pmImpact: string | null, // Business/legal/UX consequences
|
|
337
|
+
pmEffort: string | null, // "quick-win" | "medium" | "strategic"
|
|
338
|
+
|
|
311
339
|
// AI enrichment (only when ai.enabled ran)
|
|
312
340
|
aiEnhanced?: boolean, // true when AI enriched this finding
|
|
313
341
|
aiFixDescription?: string, // Claude-generated fix explanation
|
|
@@ -651,6 +679,18 @@ Type: `string`
|
|
|
651
679
|
|
|
652
680
|
---
|
|
653
681
|
|
|
682
|
+
### `PM_AI_SYSTEM_PROMPT`
|
|
683
|
+
|
|
684
|
+
The system prompt used for PM-audience AI enrichment. Instructs Claude to generate business-oriented summaries instead of developer-focused fixes. Used automatically when `ai.audience` is `"pm"`.
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
import { PM_AI_SYSTEM_PROMPT } from "@diegovelasquezweb/a11y-engine";
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Type: `string`
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
654
694
|
> **Note on `ai_enriched_findings` fast path**: When `getFindings()` receives a payload that contains `ai_enriched_findings` and no `screenshotUrlBuilder` option is provided, it returns `ai_enriched_findings` directly without re-normalizing the raw `findings` array. If a `screenshotUrlBuilder` is provided, normalization always runs so paths can be rewritten.
|
|
655
695
|
|
|
656
696
|
---
|
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
|
|
|
@@ -68,6 +68,9 @@ Each rule entry can include:
|
|
|
68
68
|
| `cms_notes` | CMS-specific fix guidance |
|
|
69
69
|
| `preferred_relationship_checks` | axe check IDs to prioritize for relationship hints |
|
|
70
70
|
| `guardrails` / `guardrails_overrides` | Must/must-not/verify constraints for automated fixes |
|
|
71
|
+
| `pm.summary` | One-line business impact for PM audience (e.g. "Buttons without labels block screen reader users from key actions") |
|
|
72
|
+
| `pm.impact` | Business/legal/UX consequences for non-technical stakeholders |
|
|
73
|
+
| `pm.effort` | Effort classification: `quick-win`, `medium`, or `strategic` |
|
|
71
74
|
|
|
72
75
|
When a framework is detected (e.g. `nextjs`), only the relevant framework notes are included in the output. React-based frameworks (`nextjs`, `gatsby`) resolve to `react` notes.
|
|
73
76
|
|
|
@@ -250,11 +253,21 @@ AI enrichment runs automatically when `ANTHROPIC_API_KEY` is present in the envi
|
|
|
250
253
|
|
|
251
254
|
The default system prompt instructs Claude to go beyond the generic fix: explain why the issue matters for users, reference the specific selector and violation data, and provide a more complete code example than the engine's default. The prompt can be overridden per-scan via the `AI_SYSTEM_PROMPT` env var or `options.ai.systemPrompt` in the programmatic API.
|
|
252
255
|
|
|
256
|
+
### PM audience mode
|
|
257
|
+
|
|
258
|
+
When `ai.audience` is set to `"pm"`, the AI enrichment uses a PM-specific system prompt that generates business-oriented guidance instead of developer-focused fixes. The PM prompt instructs Claude to produce:
|
|
259
|
+
|
|
260
|
+
- `pmSummary` — one-line business impact in plain language
|
|
261
|
+
- `pmImpact` — 2-3 sentences on legal/compliance/UX consequences
|
|
262
|
+
- `pmEffort` — `quick-win`, `medium`, or `strategic` with time estimate
|
|
263
|
+
|
|
264
|
+
These AI-generated PM fields override the static PM fields from the intelligence database for Critical and Serious findings, similar to how `fixDescription` overrides the static fix for dev audience.
|
|
265
|
+
|
|
253
266
|
## Assets Reference
|
|
254
267
|
|
|
255
268
|
| Asset | Used by | Purpose |
|
|
256
269
|
| :--- | :--- | :--- |
|
|
257
|
-
| `remediation/intelligence.mjs` | analyzer | Per-rule fix descriptions, code, guardrails, framework notes |
|
|
270
|
+
| `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
271
|
| `remediation/axe-check-maps.mjs` | analyzer | Failure mode and relationship hint mappings from axe check IDs |
|
|
259
272
|
| `remediation/guardrails.mjs` | analyzer | Shared guardrail constraints for safe automated fixes |
|
|
260
273
|
| `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
|
|
@@ -181,6 +185,9 @@ The primary enriched data artifact. Written by `src/enrichment/analyzer.mjs`. Th
|
|
|
181
185
|
| `verification_command_fallback` | `string\|null` | Fallback verify command |
|
|
182
186
|
| `pages_affected` | `number\|null` | Number of pages with this violation |
|
|
183
187
|
| `affected_urls` | `string[]\|null` | All URLs where this violation appears |
|
|
188
|
+
| `pm_summary` | `string\|null` | One-line business impact for PM audience. Populated from the intelligence database for all findings. |
|
|
189
|
+
| `pm_impact` | `string\|null` | Business/legal/UX consequences for non-technical stakeholders. |
|
|
190
|
+
| `pm_effort` | `string\|null` | Effort classification: `quick-win`, `medium`, or `strategic`. |
|
|
184
191
|
| `aiEnhanced` | `boolean` | `true` when Claude improved the fix for this finding. Only present on AI-enriched findings. |
|
|
185
192
|
| `ai_fix_description` | `string\|null` | Claude-generated fix description. More specific than `fix_description` — references the actual selector, colors, and violation data. Only present when `aiEnhanced` is `true`. |
|
|
186
193
|
| `ai_fix_code` | `string\|null` | Claude-generated code snippet in the correct framework syntax. Separate from the engine's `fix_code`. Only present when `aiEnhanced` is `true`. |
|
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[];
|
|
@@ -457,6 +510,7 @@ export function getSourcePatterns(
|
|
|
457
510
|
export function getKnowledge(options?: KnowledgeOptions): EngineKnowledge;
|
|
458
511
|
|
|
459
512
|
export const DEFAULT_AI_SYSTEM_PROMPT: string;
|
|
513
|
+
export const PM_AI_SYSTEM_PROMPT: string;
|
|
460
514
|
|
|
461
515
|
export interface ViewportPreset {
|
|
462
516
|
label: string;
|
|
@@ -472,4 +526,5 @@ export interface AiOptions {
|
|
|
472
526
|
githubToken?: string;
|
|
473
527
|
model?: string;
|
|
474
528
|
systemPrompt?: string;
|
|
529
|
+
audience?: "pm" | "dev";
|
|
475
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
|
|
@@ -688,6 +694,8 @@ export async function runAudit(options) {
|
|
|
688
694
|
projectDir: options.projectDir,
|
|
689
695
|
remotePackageJson,
|
|
690
696
|
engines,
|
|
697
|
+
clearCache: options.clearCache ?? false,
|
|
698
|
+
serverMode: options.serverMode ?? false,
|
|
691
699
|
},
|
|
692
700
|
{ onProgress },
|
|
693
701
|
);
|
|
@@ -793,6 +801,7 @@ export async function runAudit(options) {
|
|
|
793
801
|
apiKey: aiOptions.apiKey,
|
|
794
802
|
githubToken: aiOptions.githubToken || options.githubToken,
|
|
795
803
|
model: aiOptions.model,
|
|
804
|
+
audience: aiOptions.audience || "dev",
|
|
796
805
|
}
|
|
797
806
|
);
|
|
798
807
|
|