@diegovelasquezweb/a11y-engine 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # @diegovelasquezweb/a11y-engine
1
+ # a11y Engine
2
+
3
+ [![npm](https://img.shields.io/npm/v/@diegovelasquezweb/a11y-engine)](https://www.npmjs.com/package/@diegovelasquezweb/a11y-engine)
2
4
 
3
5
  Accessibility automation engine for web applications. It orchestrates multi engine scanning, stack aware enrichment, and report generation for apps and services through a stable API.
4
6
 
@@ -12,7 +14,8 @@ Accessibility automation engine for web applications. It orchestrates multi engi
12
14
  | **Fix intelligence** | Enriches each finding with WCAG mapping, fix code snippets, framework and CMS specific notes, UI library ownership hints, effort estimates, and persona impact |
13
15
  | **AI enrichment** | Optional Claude-powered analysis that adds contextual fix suggestions based on detected stack, repo structure, and finding patterns |
14
16
  | **Report generation** | Produces HTML dashboard, PDF compliance report, manual testing checklist, and Markdown remediation guide |
15
- | **Source code scanning** | Static regex analysis of project source for accessibility patterns that runtime engines cannot detect works with local paths or remote GitHub repos |
17
+ | **Source code scanning** | Static regex analysis of project source for accessibility patterns that runtime engines cannot detect. Works with local paths or remote GitHub repos |
18
+ | **Knowledge API** | Exposes WCAG conformance levels, severity definitions, persona profiles, glossary, scanner help, and documentation so frontends and agents can render guidance from engine-owned data |
16
19
 
17
20
  ## Installation
18
21
 
@@ -58,13 +61,13 @@ const payload = await runAudit({
58
61
  maxRoutes: 5,
59
62
  axeTags: ["wcag2a", "wcag2aa", "best-practice"],
60
63
  engines: { axe: true, cdp: true, pa11y: true },
61
- repoUrl: "https://github.com/owner/repo", // optional enables source pattern scan and stack detection from package.json
62
- githubToken: process.env.GH_TOKEN, // optional for private repos and higher GitHub API rate limits
64
+ repoUrl: "https://github.com/owner/repo", // optional, enables source pattern scan and stack detection
65
+ githubToken: process.env.GH_TOKEN, // optional, required for private repos
63
66
  ai: {
64
67
  enabled: true,
65
68
  apiKey: process.env.ANTHROPIC_API_KEY,
66
69
  githubToken: process.env.GH_TOKEN,
67
- systemPrompt: "Custom prompt...", // optional overrides default Claude system prompt
70
+ systemPrompt: "Custom prompt...", // optional, overrides default Claude system prompt
68
71
  },
69
72
  onProgress: (step, status, extra) => console.log(`${step}: ${status}`, extra),
70
73
  });
@@ -135,25 +138,7 @@ See [API Reference](docs/api-reference.md) for exact options and return types.
135
138
 
136
139
  ## CLI
137
140
 
138
- The package exposes an `a11y-audit` binary for terminal execution.
139
-
140
- ```bash
141
- # Basic scan
142
- pnpm exec a11y-audit --base-url https://example.com
143
-
144
- # With source code pattern scanning via GitHub API (no clone)
145
- pnpm exec a11y-audit --base-url https://example.com \
146
- --repo-url https://github.com/owner/repo \
147
- --github-token ghp_...
148
-
149
- # With AI enrichment (set ANTHROPIC_API_KEY env var)
150
- ANTHROPIC_API_KEY=sk-ant-... pnpm exec a11y-audit --base-url https://example.com
151
-
152
- # With custom AI system prompt
153
- AI_SYSTEM_PROMPT="You are..." ANTHROPIC_API_KEY=sk-ant-... pnpm exec a11y-audit --base-url https://example.com
154
- ```
155
-
156
- See the [CLI Handbook](docs/cli-handbook.md) for all flags and examples.
141
+ The package exposes an `a11y-audit` binary for terminal execution. See the [CLI Handbook](docs/cli-handbook.md) for all flags, env vars, and examples.
157
142
 
158
143
  ## AI enrichment
159
144
 
@@ -163,7 +148,7 @@ When `ANTHROPIC_API_KEY` is set, the engine runs a post-scan enrichment step tha
163
148
  - A production-quality code snippet in the correct framework syntax
164
149
  - Context-aware suggestions when repo source files are available
165
150
 
166
- AI output is stored in separate fields (`ai_fix_description`, `ai_fix_code`) the original engine fixes are always preserved. Findings improved by AI are flagged with `aiEnhanced: true`.
151
+ AI output is stored in separate fields (`ai_fix_description`, `ai_fix_code`). The original engine fixes are always preserved. Findings improved by AI are flagged with `aiEnhanced: true`.
167
152
 
168
153
  The system prompt is fully customizable via `options.ai.systemPrompt` (programmatic API) or the `AI_SYSTEM_PROMPT` env var (CLI).
169
154
 
@@ -4,174 +4,444 @@
4
4
 
5
5
  ---
6
6
 
7
- ## Core API
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Import](#import)
11
+ - [End-to-end example](#end-to-end-example)
12
+ - [Core API](#core-api)
13
+ - [runAudit](#runauditoptions)
14
+ - [getFindings](#getfindingsinput-options)
15
+ - [getOverview](#getoverviewfindings-payload)
16
+ - [Output API](#output-api)
17
+ - [getPDFReport](#getpdfreportpayload-options)
18
+ - [getHTMLReport](#gethtmlreportpayload-options)
19
+ - [getChecklist](#getchecklistoptions)
20
+ - [getRemediationGuide](#getremediationguidepayload-options)
21
+ - [getSourcePatterns](#getsourcepatternsprojdir-options)
22
+ - [Knowledge API](#knowledge-api)
23
+ - [getKnowledge](#getknowledgeoptions)
24
+ - [getScannerHelp](#getscannerhelpoptions)
25
+ - [getPersonaReference](#getpersonareferenceoptions)
26
+ - [getUiHelp](#getuihelpoptions)
27
+ - [getConformanceLevels](#getconformancelevelsoptions)
28
+ - [getWcagPrinciples](#getwcagprinciplesoptions)
29
+ - [getSeverityLevels](#getseveritylevelsoptions)
8
30
 
9
- ### `runAudit(options)`
31
+ ---
10
32
 
11
- Runs route discovery, runtime scan, merge, analyzer enrichment, and optional AI enrichment. Supports local project paths or remote GitHub repos for stack detection and source pattern scanning.
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @diegovelasquezweb/a11y-engine
37
+ npx playwright install chromium
38
+ npx puppeteer browsers install chrome
39
+ ```
40
+
41
+ ## Import
42
+
43
+ All functions are named exports from the package root:
44
+
45
+ ```ts
46
+ import {
47
+ runAudit,
48
+ getFindings,
49
+ getOverview,
50
+ getPDFReport,
51
+ getHTMLReport,
52
+ getChecklist,
53
+ getRemediationGuide,
54
+ getSourcePatterns,
55
+ getKnowledge,
56
+ getScannerHelp,
57
+ getPersonaReference,
58
+ getUiHelp,
59
+ getConformanceLevels,
60
+ getWcagPrinciples,
61
+ getSeverityLevels,
62
+ } from "@diegovelasquezweb/a11y-engine";
63
+ ```
12
64
 
13
- `options` (`RunAuditOptions`):
65
+ ---
14
66
 
15
- | Option | Type |
16
- | :--- | :--- |
17
- | `baseUrl` | `string` |
18
- | `maxRoutes` | `number` |
19
- | `crawlDepth` | `number` |
20
- | `routes` | `string` |
21
- | `waitMs` | `number` |
22
- | `timeoutMs` | `number` |
23
- | `headless` | `boolean` |
24
- | `waitUntil` | `string` |
25
- | `colorScheme` | `string` |
26
- | `viewport` | `{ width: number; height: number }` |
27
- | `axeTags` | `string[]` |
28
- | `onlyRule` | `string` |
29
- | `excludeSelectors` | `string[]` |
30
- | `ignoreFindings` | `string[]` |
31
- | `framework` | `string` |
32
- | `projectDir` | `string` |
33
- | `repoUrl` | `string` |
34
- | `githubToken` | `string` |
35
- | `skipPatterns` | `boolean` |
36
- | `screenshotsDir` | `string` |
37
- | `engines` | `{ axe?: boolean; cdp?: boolean; pa11y?: boolean }` |
38
- | `ai` | `{ enabled?: boolean; apiKey?: string; githubToken?: string; model?: string; systemPrompt?: string }` — `systemPrompt` overrides the default Claude prompt when set |
39
- | `onProgress` | `(step: string, status: string, extra?: Record<string, unknown>) => void` |
40
-
41
- Progress steps emitted via `onProgress`:
42
-
43
- | Step | When |
67
+ ## End-to-end example
68
+
69
+ ```ts
70
+ import { runAudit, getFindings, getOverview } from "@diegovelasquezweb/a11y-engine";
71
+
72
+ // 1. Run the scan
73
+ const payload = await runAudit({
74
+ baseUrl: "https://example.com",
75
+ maxRoutes: 5,
76
+ engines: { axe: true, cdp: true, pa11y: true },
77
+ onProgress: (step, status) => console.log(`[${step}] ${status}`),
78
+ });
79
+
80
+ // 2. Get enriched findings
81
+ const findings = getFindings(payload);
82
+
83
+ // 3. Get compliance summary
84
+ const { score, scoreLabel, wcagStatus, totals, quickWins } = getOverview(findings, payload);
85
+
86
+ console.log(`Score: ${score}/100 (${scoreLabel})`);
87
+ console.log(`WCAG Status: ${wcagStatus}`);
88
+ console.log(`Critical: ${totals.Critical}, Serious: ${totals.Serious}`);
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Core API
94
+
95
+ ### `runAudit(options)`
96
+
97
+ Runs the full audit pipeline: route discovery → axe/CDP/pa11y scan → merge/dedup → analyzer enrichment → optional source pattern scanning → optional AI enrichment.
98
+
99
+ Returns a `ScanPayload` object consumed by all other functions.
100
+
101
+ **Options:**
102
+
103
+ | Option | Type | Default | Range / Values | Description |
104
+ | :--- | :--- | :--- | :--- | :--- |
105
+ | `baseUrl` | `string` | — | Required | Starting URL including protocol (`https://` or `http://`) |
106
+ | `maxRoutes` | `number` | `10` | 1 – 50 | Maximum unique pages to discover and scan |
107
+ | `crawlDepth` | `number` | `2` | 1 – 3 | BFS link-follow depth from `baseUrl`. Has no effect when `routes` is set |
108
+ | `routes` | `string` | — | CSV paths | Explicit paths to scan (e.g. `"/,/about,/contact"`). Overrides auto-discovery entirely |
109
+ | `waitUntil` | `string` | `"domcontentloaded"` | `"domcontentloaded"` \| `"load"` \| `"networkidle"` | Page load strategy. Use `"networkidle"` for SPAs that render after `DOMContentLoaded` |
110
+ | `waitMs` | `number` | `2000` | 0 – 10000 | Fixed delay (ms) after page load before engines run |
111
+ | `timeoutMs` | `number` | `30000` | 5000 – 120000 | Network timeout per page (ms) |
112
+ | `headless` | `boolean` | `true` | — | Set `false` to open a visible browser for debugging |
113
+ | `viewport` | `object` | `{ width: 1280, height: 800 }` | width: 320–2560, height: 320–2560 | Browser viewport dimensions in pixels |
114
+ | `colorScheme` | `string` | `"light"` | `"light"` \| `"dark"` | Emulates `prefers-color-scheme` media query |
115
+ | `engines` | `object` | all `true` | `{ axe?, cdp?, pa11y? }` | Which engines to run. At least one must be enabled |
116
+ | `axeTags` | `string[]` | WCAG 2.x A+AA | See below | axe-core rule tag filter. Also determines pa11y standard |
117
+ | `onlyRule` | `string` | — | axe rule ID | Run a single axe rule only (e.g. `"color-contrast"`) |
118
+ | `ignoreFindings` | `string[]` | — | axe rule IDs | Suppress specific rules from output entirely |
119
+ | `excludeSelectors` | `string[]` | — | CSS selectors | Skip elements matching these selectors during axe scan |
120
+ | `framework` | `string` | auto-detected | See below | Override framework detection for fix notes and source boundaries |
121
+ | `projectDir` | `string` | — | local path | Local project source directory. Enables source pattern scanning and package.json stack detection |
122
+ | `repoUrl` | `string` | — | GitHub URL | Remote repo URL. Enables source pattern scanning via GitHub API — no clone required |
123
+ | `githubToken` | `string` | — | GitHub PAT | Increases GitHub API rate limit from 60 to 5,000 req/hr. Required for private repos |
124
+ | `skipPatterns` | `boolean` | `false` | — | Disable source pattern scanning even when `projectDir` or `repoUrl` is set |
125
+ | `screenshotsDir` | `string` | `.audit/screenshots` | dir path | Directory where element screenshots are saved |
126
+ | `ai.enabled` | `boolean` | `false` | — | Enable Claude AI enrichment for Critical and Serious findings |
127
+ | `ai.apiKey` | `string` | — | Anthropic API key | Required when `ai.enabled` is `true` |
128
+ | `ai.githubToken` | `string` | — | GitHub PAT | Used to fetch source files from the repo for AI context |
129
+ | `ai.model` | `string` | `"claude-haiku-4-5-20251001"` | Anthropic model ID | Claude model to use |
130
+ | `ai.systemPrompt` | `string` | Built-in prompt | — | Overrides the default Claude system prompt for the entire scan |
131
+ | `onProgress` | `function` | — | — | Callback fired at each pipeline step |
132
+
133
+ **`axeTags` common values:**
134
+
135
+ | Tag | Covers |
44
136
  | :--- | :--- |
45
- | `page` | Always page load |
46
- | `axe` | Always axe-core scan |
47
- | `cdp` | Always CDP accessibility tree check |
48
- | `pa11y` | Always pa11y HTML CodeSniffer scan |
49
- | `merge` | Always finding deduplication |
50
- | `intelligence` | Always enrichment and WCAG mapping |
51
- | `repo` | When `repoUrl` is set |
52
- | `patterns` | When source scanning is active |
53
- | `ai` | When AI enrichment is configured |
137
+ | `wcag2a` | WCAG 2.0 Level A |
138
+ | `wcag2aa` | WCAG 2.0 Level AA |
139
+ | `wcag21a` | WCAG 2.1 Level A additions |
140
+ | `wcag21aa` | WCAG 2.1 Level AA additions |
141
+ | `wcag22a` | WCAG 2.2 Level A additions |
142
+ | `wcag22aa` | WCAG 2.2 Level AA additions |
143
+ | `wcag2aaa` | WCAG 2.0 Level AAA |
144
+ | `best-practice` | Non-WCAG best practices |
145
+
146
+ **Supported `framework` values:** `nextjs`, `gatsby`, `react`, `nuxt`, `vue`, `angular`, `astro`, `svelte`, `remix`, `shopify`, `wordpress`, `drupal`
147
+
148
+ **`onProgress` callback:**
149
+
150
+ ```ts
151
+ onProgress: (step, status, extra) => {
152
+ // step: "page" | "axe" | "cdp" | "pa11y" | "merge" | "intelligence" | "repo" | "patterns" | "ai"
153
+ // status: "running" | "done" | "error" | "skipped"
154
+ // extra: { found?: number, merged?: number, ... } — step-specific data
155
+ }
156
+ ```
157
+
158
+ ```ts
159
+ const payload = await runAudit({
160
+ baseUrl: "https://example.com",
161
+ maxRoutes: 5,
162
+ engines: { axe: true, cdp: true, pa11y: true },
163
+ repoUrl: "https://github.com/owner/repo",
164
+ githubToken: process.env.GH_TOKEN,
165
+ ai: { enabled: true, apiKey: process.env.ANTHROPIC_API_KEY },
166
+ onProgress: (step, status) => console.log(`[${step}] ${status}`),
167
+ });
168
+ ```
54
169
 
55
170
  Returns: `Promise<ScanPayload>`
56
171
 
57
- > **`ai_enriched_findings` fast path**: When AI enrichment runs, the engine appends `ai_enriched_findings` to the payload. `getFindings()` checks for this field first — if present, it returns the already-enriched findings directly without re-normalizing the raw `findings` array.
172
+ > **`ai_enriched_findings` fast path**: When AI enrichment runs, `getFindings()` uses `payload.ai_enriched_findings` directly instead of re-normalizing the raw findings array.
58
173
 
59
- ### `getFindings(input, options?)`
174
+ ---
60
175
 
61
- Normalizes and enriches findings and returns sorted enriched findings.
176
+ ### `getFindings(input, options?)`
62
177
 
63
- - `input`: `ScanPayload` from `runAudit`
64
- - `options` (`EnrichmentOptions`):
65
- - `screenshotUrlBuilder?: (rawPath: string) => string`
178
+ Normalizes raw scan results into enriched, UI-ready findings sorted by severity.
179
+
180
+ ```ts
181
+ import { getFindings } from "@diegovelasquezweb/a11y-engine";
182
+
183
+ const findings = getFindings(payload, {
184
+ // Optional: rewrite internal screenshot paths to app URLs
185
+ screenshotUrlBuilder: (rawPath) =>
186
+ `/api/scan/${scanId}/screenshot?path=${encodeURIComponent(rawPath)}`,
187
+ });
188
+
189
+ // findings[0] example:
190
+ // {
191
+ // id: "A11Y-001",
192
+ // ruleId: "color-contrast",
193
+ // title: "Elements must meet minimum color contrast ratio thresholds",
194
+ // severity: "Serious",
195
+ // wcag: "1.4.3",
196
+ // selector: ".hero-text",
197
+ // actual: "Element has insufficient color contrast of 2.5:1 ...",
198
+ // expected: "Text contrast ratio must be at least 4.5:1 ...",
199
+ // fixDescription: "Increase the foreground color contrast ...",
200
+ // fixCode: "/* Change #aaa to #767676 */",
201
+ // effort: "low",
202
+ // aiEnhanced: true, // present when AI ran
203
+ // aiFixDescription: "...", // Claude-generated (more specific)
204
+ // aiFixCode: "...", // Claude-generated code snippet
205
+ // }
206
+ ```
66
207
 
67
208
  Returns: `EnrichedFinding[]`
68
209
 
69
- ### `getOverview(findings, payload?)`
210
+ ---
70
211
 
71
- Computes totals, score, WCAG status, persona groups, quick wins, target URL, and detected stack.
212
+ ### `getOverview(findings, payload?)`
72
213
 
73
- - `findings`: `EnrichedFinding[]`
74
- - `payload`: `ScanPayload | null`
214
+ Computes the compliance score, WCAG status, severity totals, persona groups, and quick wins from enriched findings.
215
+
216
+ ```ts
217
+ import { getFindings, getOverview } from "@diegovelasquezweb/a11y-engine";
218
+
219
+ const findings = getFindings(payload);
220
+ const overview = getOverview(findings, payload);
221
+
222
+ // overview example:
223
+ // {
224
+ // score: 72, // 0–100. Formula: 100 - (Critical×15) - (Serious×5) - (Moderate×2) - (Minor×0.5)
225
+ // label: "Fair", // "Excellent" (90–100) | "Good" (75–89) | "Fair" (55–74) | "Poor" (35–54) | "Critical" (0–34)
226
+ // wcagStatus: "Fail", // "Pass" | "Conditional Pass" | "Fail"
227
+ // totals: { Critical: 1, Serious: 3, Moderate: 5, Minor: 2 },
228
+ // personaGroups: {
229
+ // screenReader: { count: 4, findings: [...] },
230
+ // keyboard: { count: 2, findings: [...] },
231
+ // vision: { count: 3, findings: [...] },
232
+ // cognitive: { count: 1, findings: [...] },
233
+ // },
234
+ // quickWins: [...], // top Critical/Serious findings with fix code ready
235
+ // targetUrl: "https://example.com",
236
+ // detectedStack: { framework: "nextjs", cms: null, uiLibraries: ["radix-ui"] },
237
+ // }
238
+ ```
75
239
 
76
240
  Returns: `AuditSummary`
77
241
 
242
+ ---
243
+
78
244
  ## Output API
79
245
 
80
246
  ### `getPDFReport(payload, options?)`
81
247
 
82
- - `payload`: `ScanPayload`
83
- - `options`: `ReportOptions`
84
- - `baseUrl?: string`
85
- - `target?: string`
248
+ Generates a formal A4 PDF compliance report.
249
+
250
+ ```ts
251
+ import { getPDFReport } from "@diegovelasquezweb/a11y-engine";
252
+
253
+ const { buffer, contentType } = await getPDFReport(payload, {
254
+ baseUrl: "https://example.com",
255
+ target: "WCAG 2.2 AA",
256
+ });
86
257
 
87
- Returns: `Promise<PDFReport>` (`{ buffer, contentType }`)
258
+ // In a Next.js API route:
259
+ return new Response(buffer, { headers: { "Content-Type": contentType } });
260
+ ```
261
+
262
+ Returns: `Promise<{ buffer: Buffer, contentType: string }>`
263
+
264
+ ---
88
265
 
89
266
  ### `getHTMLReport(payload, options?)`
90
267
 
91
- - `payload`: `ScanPayload`
92
- - `options`: `HTMLReportOptions`
93
- - `baseUrl?: string`
94
- - `target?: string`
95
- - `screenshotsDir?: string`
268
+ Generates an interactive HTML audit dashboard with finding cards, score gauge, and persona breakdown.
269
+
270
+ ```ts
271
+ import { getHTMLReport } from "@diegovelasquezweb/a11y-engine";
96
272
 
97
- Returns: `Promise<HTMLReport>` (`{ html, contentType }`)
273
+ const { html, contentType } = await getHTMLReport(payload, {
274
+ baseUrl: "https://example.com",
275
+ screenshotsDir: "/path/to/screenshots",
276
+ });
277
+ ```
278
+
279
+ Returns: `Promise<{ html: string, contentType: string }>`
280
+
281
+ ---
98
282
 
99
283
  ### `getChecklist(options?)`
100
284
 
101
- - `options`: `Pick<ReportOptions, "baseUrl">`
102
- - `baseUrl?: string`
285
+ Generates an interactive HTML manual testing checklist with 41 WCAG checks.
286
+
287
+ ```ts
288
+ import { getChecklist } from "@diegovelasquezweb/a11y-engine";
103
289
 
104
- Returns: `Promise<ChecklistReport>` (`{ html, contentType }`)
290
+ const { html, contentType } = await getChecklist({
291
+ baseUrl: "https://example.com",
292
+ });
293
+ ```
294
+
295
+ Returns: `Promise<{ html: string, contentType: string }>`
296
+
297
+ ---
105
298
 
106
299
  ### `getRemediationGuide(payload, options?)`
107
300
 
108
- - `payload`: `ScanPayload & { incomplete_findings?: unknown[] }`
109
- - `options`: `RemediationOptions`
110
- - `baseUrl?: string`
111
- - `target?: string`
112
- - `patternFindings?: Record<string, unknown> | null`
301
+ Generates a Markdown remediation guide optimized for AI agents and developers. Includes finding details, fix code, verify commands, and source pattern findings.
302
+
303
+ ```ts
304
+ import { getRemediationGuide } from "@diegovelasquezweb/a11y-engine";
305
+
306
+ const { markdown, contentType } = await getRemediationGuide(payload, {
307
+ baseUrl: "https://example.com",
308
+ patternFindings: payload.patternFindings ?? null,
309
+ });
113
310
 
114
- Returns: `Promise<RemediationGuide>` (`{ markdown, contentType }`)
311
+ // Write to disk or return as download
312
+ ```
313
+
314
+ Returns: `Promise<{ markdown: string, contentType: string }>`
315
+
316
+ ---
115
317
 
116
318
  ### `getSourcePatterns(projectDir, options?)`
117
319
 
118
- - `projectDir`: `string`
119
- - `options`: `SourcePatternOptions`
120
- - `framework?: string`
121
- - `onlyPattern?: string`
320
+ Scans a local project directory for source code accessibility patterns that runtime engines cannot detect.
321
+
322
+ ```ts
323
+ import { getSourcePatterns } from "@diegovelasquezweb/a11y-engine";
324
+
325
+ const result = await getSourcePatterns("./", {
326
+ framework: "nextjs", // optional — scopes scan to framework source dirs
327
+ onlyPattern: "placeholder-only-label", // optional — run a single pattern
328
+ });
329
+
330
+ // result example:
331
+ // {
332
+ // findings: [
333
+ // {
334
+ // id: "PAT-a1b2c3",
335
+ // pattern_id: "placeholder-only-label",
336
+ // title: "Input uses placeholder as its only label",
337
+ // severity: "Critical",
338
+ // status: "confirmed",
339
+ // file: "src/components/SearchBar.tsx",
340
+ // line: 12,
341
+ // match: ' <input placeholder="Search..." />',
342
+ // context: "...",
343
+ // fix_description: "Add an aria-label or visible <label> element",
344
+ // }
345
+ // ],
346
+ // summary: { total: 3, confirmed: 2, potential: 1 }
347
+ // }
348
+ ```
122
349
 
123
350
  Returns: `Promise<SourcePatternResult>`
124
351
 
352
+ ---
353
+
125
354
  ## Knowledge API
126
355
 
356
+ These functions expose engine-owned content for UIs and agents to render. All accept an optional `{ locale?: string }` option (default: `"en"`).
357
+
358
+ ### `getKnowledge(options?)`
359
+
360
+ Returns the full knowledge pack — combines all knowledge functions into one call. Useful for pre-loading UI help content.
361
+
362
+ ```ts
363
+ import { getKnowledge } from "@diegovelasquezweb/a11y-engine";
364
+
365
+ const knowledge = getKnowledge({ locale: "en" });
366
+
367
+ // knowledge.scanner → scan options help and engine descriptions
368
+ // knowledge.personas → persona labels, icons, descriptions
369
+ // knowledge.concepts → UI concept definitions
370
+ // knowledge.glossary → accessibility glossary
371
+ // knowledge.conformanceLevels → WCAG A/AA/AAA definitions with axe tags
372
+ // knowledge.wcagPrinciples → the 4 WCAG principles
373
+ // knowledge.severityLevels → Critical/Serious/Moderate/Minor definitions
374
+ ```
375
+
376
+ Returns: `EngineKnowledge`
377
+
378
+ ---
379
+
127
380
  ### `getScannerHelp(options?)`
128
381
 
129
- - `options`: `KnowledgeOptions`
130
- - `locale?: string`
382
+ Returns scan option descriptions, allowed values, and engine metadata — used to render Advanced Settings UI.
131
383
 
132
- Returns: `ScannerHelp` (`{ locale, version, title, engines, options }`)
384
+ ```ts
385
+ const help = getScannerHelp();
386
+ // help.engines → [{ id: "axe", label: "axe-core", description: "..." }, ...]
387
+ // help.options → [{ id: "maxRoutes", label: "Max Routes", type: "number", ... }, ...]
388
+ ```
389
+
390
+ ---
133
391
 
134
392
  ### `getPersonaReference(options?)`
135
393
 
136
- - `options`: `KnowledgeOptions`
137
- - `locale?: string`
394
+ Returns persona labels, descriptions, and disability group definitions.
138
395
 
139
- Returns: `PersonaReference` (`{ locale, version, personas }`)
396
+ ```ts
397
+ const ref = getPersonaReference();
398
+ // ref.personas → [{ id: "screenReader", label: "Screen Readers", icon: "...", description: "..." }, ...]
399
+ ```
400
+
401
+ ---
140
402
 
141
403
  ### `getUiHelp(options?)`
142
404
 
143
- - `options`: `KnowledgeOptions`
144
- - `locale?: string`
405
+ Returns shared concept definitions and a glossary of accessibility terms.
145
406
 
146
- Returns: `UiHelp` (`{ locale, version, concepts, glossary }`)
407
+ ```ts
408
+ const ui = getUiHelp();
409
+ // ui.concepts → { wcag: "...", aria: "...", ... }
410
+ // ui.glossary → [{ term: "ARIA", definition: "..." }, ...]
411
+ ```
147
412
 
148
- ### `getConformanceLevels(options?)`
413
+ ---
149
414
 
150
- - `options`: `KnowledgeOptions`
151
- - `locale?: string`
415
+ ### `getConformanceLevels(options?)`
152
416
 
153
- Returns: `ConformanceLevelsResult` (`{ locale, version, conformanceLevels }`)
417
+ Returns WCAG conformance level definitions with their corresponding axe-core tag sets.
154
418
 
155
- ### `getWcagPrinciples(options?)`
419
+ ```ts
420
+ const { conformanceLevels } = getConformanceLevels();
421
+ // conformanceLevels[0] → { id: "AA", label: "WCAG 2.2 AA", axeTags: ["wcag2a", "wcag2aa", ...] }
422
+ ```
156
423
 
157
- - `options`: `KnowledgeOptions`
158
- - `locale?: string`
424
+ ---
159
425
 
160
- Returns: `WcagPrinciplesResult` (`{ locale, version, wcagPrinciples }`)
426
+ ### `getWcagPrinciples(options?)`
161
427
 
162
- ### `getSeverityLevels(options?)`
428
+ Returns the four WCAG principles (Perceivable, Operable, Understandable, Robust) with criterion prefix patterns.
163
429
 
164
- - `options`: `KnowledgeOptions`
165
- - `locale?: string`
430
+ ```ts
431
+ const { wcagPrinciples } = getWcagPrinciples();
432
+ // wcagPrinciples[0] → { id: "perceivable", label: "Perceivable", prefix: "1.", description: "..." }
433
+ ```
166
434
 
167
- Returns: `SeverityLevelsResult` (`{ locale, version, severityLevels }`)
435
+ ---
168
436
 
169
- ### `getKnowledge(options?)`
437
+ ### `getSeverityLevels(options?)`
170
438
 
171
- - `options`: `KnowledgeOptions`
172
- - `locale?: string`
439
+ Returns severity level definitions with labels, descriptions, and ordering.
173
440
 
174
- Returns: `EngineKnowledge` (`{ locale, version, scanner, personas, concepts, glossary, docs, conformanceLevels, wcagPrinciples, severityLevels }`)
441
+ ```ts
442
+ const { severityLevels } = getSeverityLevels();
443
+ // severityLevels[0] → { id: "Critical", label: "Critical", order: 0, description: "..." }
444
+ ```
175
445
 
176
446
  ---
177
447
 
@@ -4,6 +4,14 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## Table of Contents
8
+
9
+ - [Source Modules](#1-source-modules)
10
+ - [Asset Modules](#2-asset-modules)
11
+ - [Test Suite](#3-test-suite)
12
+
13
+ ---
14
+
7
15
  This document is the current technical inventory of the engine package.
8
16
 
9
17
  ## 1) Source Modules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ai/claude.mjs CHANGED
@@ -25,18 +25,10 @@ const MAX_AI_FINDINGS = 20; // cap to control cost
25
25
  * @param {object} context
26
26
  * @returns {string}
27
27
  */
28
- function buildSystemPrompt(context) {
29
- const { framework, cms, uiLibraries } = context.stack || {};
30
-
31
- let stackInfo = "";
32
- if (framework) stackInfo += `Framework: ${framework}\n`;
33
- if (cms) stackInfo += `CMS: ${cms}\n`;
34
- if (uiLibraries?.length) stackInfo += `UI Libraries: ${uiLibraries.join(", ")}\n`;
35
-
36
- return `You are an expert web accessibility engineer specializing in WCAG 2.2 AA remediation.
28
+ export const DEFAULT_AI_SYSTEM_PROMPT = `You are an expert web accessibility engineer specializing in WCAG 2.2 AA remediation.
37
29
 
38
30
  Your task is to provide a developer-friendly AI hint for each accessibility finding — something MORE USEFUL than the generic automated fix already provided.
39
- ${stackInfo ? `\nProject context:\n${stackInfo}` : ""}
31
+
40
32
  For each finding, provide:
41
33
  1. fixDescription: A 2-3 sentence explanation that goes BEYOND the generic fix. Explain WHY this matters for real users, WHAT specifically to look for in the codebase, and HOW to verify the fix works. Be specific to the selector and actual violation data provided.
42
34
  2. fixCode: A ready-to-use, production-quality code snippet in the correct syntax for the stack. Do NOT copy the existing fix code — write a BETTER, more complete example that a developer can use directly.
@@ -48,6 +40,20 @@ Rules:
48
40
  - Reference the actual selector or element from the finding when possible
49
41
  - If the violation data contains specific values (colors, ratios, labels), use them in your response
50
42
  - Respond in JSON only — no markdown, no explanation outside the JSON structure`;
43
+
44
+ function buildSystemPrompt(context) {
45
+ const { framework, cms, uiLibraries } = context.stack || {};
46
+
47
+ let stackInfo = "";
48
+ if (framework) stackInfo += `Framework: ${framework}\n`;
49
+ if (cms) stackInfo += `CMS: ${cms}\n`;
50
+ if (uiLibraries?.length) stackInfo += `UI Libraries: ${uiLibraries.join(", ")}\n`;
51
+
52
+ const base = DEFAULT_AI_SYSTEM_PROMPT;
53
+ return stackInfo ? base.replace(
54
+ "For each finding, provide:",
55
+ `Project context:\n${stackInfo}\nFor each finding, provide:`
56
+ ) : base;
51
57
  }
52
58
 
53
59
  /**
@@ -230,7 +236,7 @@ async function fetchSourceFilesForFindings(findings, repoUrl, githubToken) {
230
236
  try {
231
237
  const content = await fetchRepoFile(repoUrl, filePath, githubToken);
232
238
  if (content) sourceFiles[filePath] = content;
233
- } catch { /* non-fatal */ }
239
+ } catch { }
234
240
  }
235
241
  }
236
242
 
package/src/ai/enrich.mjs CHANGED
@@ -74,6 +74,6 @@ async function main() {
74
74
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
75
75
  main().catch((err) => {
76
76
  log.warn(`AI enrichment failed (non-fatal): ${err.message}`);
77
- process.exit(0); // non-fatal — never block the pipeline
77
+ process.exit(0);
78
78
  });
79
79
  }
package/src/index.d.mts CHANGED
@@ -403,15 +403,6 @@ export interface RunAuditOptions {
403
403
  onProgress?: (step: string, status: string, extra?: Record<string, unknown>) => void;
404
404
  }
405
405
 
406
- export interface AiOptions {
407
- enabled?: boolean;
408
- apiKey?: string;
409
- githubToken?: string;
410
- model?: string;
411
- }
412
-
413
-
414
-
415
406
 
416
407
 
417
408
  export interface EnrichmentOptions {
@@ -471,3 +462,21 @@ export function getWcagPrinciples(options?: KnowledgeOptions): WcagPrinciplesRes
471
462
  export function getSeverityLevels(options?: KnowledgeOptions): SeverityLevelsResult;
472
463
 
473
464
  export function getKnowledge(options?: KnowledgeOptions): EngineKnowledge;
465
+
466
+ export const DEFAULT_AI_SYSTEM_PROMPT: string;
467
+
468
+ export interface ViewportPreset {
469
+ label: string;
470
+ width: number;
471
+ height: number;
472
+ }
473
+
474
+ export const VIEWPORT_PRESETS: ViewportPreset[];
475
+
476
+ export interface AiOptions {
477
+ enabled?: boolean;
478
+ apiKey?: string;
479
+ githubToken?: string;
480
+ model?: string;
481
+ systemPrompt?: string;
482
+ }
package/src/index.mjs CHANGED
@@ -5,10 +5,9 @@
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
9
 
9
- // ---------------------------------------------------------------------------
10
10
  // Lazy-loaded asset cache
11
- // ---------------------------------------------------------------------------
12
11
 
13
12
  let _intelligence = null;
14
13
  let _pa11yConfig = null;
@@ -58,9 +57,7 @@ function resolveKnowledgeLocale(locale = "en") {
58
57
  return "en";
59
58
  }
60
59
 
61
- // ---------------------------------------------------------------------------
62
60
  // Pa11y rule canonicalization (internal)
63
- // ---------------------------------------------------------------------------
64
61
 
65
62
  function normalizePa11yToken(value) {
66
63
  return value
@@ -97,9 +94,7 @@ function mapPa11yRuleToCanonical(ruleId, sourceRuleId = null, checkData = null)
97
94
  return ruleId;
98
95
  }
99
96
 
100
- // ---------------------------------------------------------------------------
101
97
  // Raw finding normalization (internal)
102
- // ---------------------------------------------------------------------------
103
98
 
104
99
  const SEVERITY_ORDER = { Critical: 1, Serious: 2, Moderate: 3, Minor: 4 };
105
100
 
@@ -169,9 +164,7 @@ function normalizeSingleFinding(item, index, screenshotUrlBuilder) {
169
164
  };
170
165
  }
171
166
 
172
- // ---------------------------------------------------------------------------
173
167
  // Finding enrichment
174
- // ---------------------------------------------------------------------------
175
168
 
176
169
  /**
177
170
  * Normalizes and enriches raw findings with intelligence data.
@@ -188,19 +181,16 @@ export function getFindings(input, options = {}) {
188
181
  const { screenshotUrlBuilder = null } = options;
189
182
  const rules = getIntelligence().rules || {};
190
183
 
191
- // If AI enrichment ran, return those findings directly (already normalized + enriched)
192
184
  if (input?.ai_enriched_findings?.length > 0 && !screenshotUrlBuilder) {
193
185
  return input.ai_enriched_findings;
194
186
  }
195
187
 
196
188
  const rawFindings = input?.findings || [];
197
189
 
198
- // Normalize raw findings
199
190
  const normalized = rawFindings.map((item, index) =>
200
191
  normalizeSingleFinding(item, index, screenshotUrlBuilder)
201
192
  );
202
193
 
203
- // Enrich with intelligence and output camelCase-only findings
204
194
  const enriched = normalized.map((finding) => {
205
195
  const canonical = mapPa11yRuleToCanonical(
206
196
  finding.rule_id,
@@ -208,7 +198,6 @@ export function getFindings(input, options = {}) {
208
198
  finding.check_data,
209
199
  );
210
200
 
211
- // Build camelCase-only enriched finding
212
201
  const enrichedFinding = {
213
202
  id: finding.id,
214
203
  ruleId: canonical,
@@ -238,7 +227,7 @@ export function getFindings(input, options = {}) {
238
227
  recommendedFix: finding.recommended_fix,
239
228
  evidence: finding.evidence,
240
229
  totalInstances: finding.total_instances,
241
- effort: finding.effort,
230
+ effort: finding.effort ?? (finding.fix_code ? "low" : "high"),
242
231
  relatedRules: finding.related_rules,
243
232
  screenshotPath: finding.screenshot_path,
244
233
  falsePositiveRisk: finding.false_positive_risk,
@@ -280,7 +269,6 @@ export function getFindings(input, options = {}) {
280
269
  return enrichedFinding;
281
270
  });
282
271
 
283
- // Sort by severity then by ID
284
272
  enriched.sort((a, b) => {
285
273
  const sa = SEVERITY_ORDER[a.severity] ?? 99;
286
274
  const sb = SEVERITY_ORDER[b.severity] ?? 99;
@@ -291,9 +279,7 @@ export function getFindings(input, options = {}) {
291
279
  return enriched;
292
280
  }
293
281
 
294
- // ---------------------------------------------------------------------------
295
282
  // Score computation (internal)
296
- // ---------------------------------------------------------------------------
297
283
 
298
284
  function getComplianceScore(totals) {
299
285
  const config = getComplianceConfig();
@@ -324,9 +310,7 @@ function getComplianceScore(totals) {
324
310
  return { score, label, wcagStatus };
325
311
  }
326
312
 
327
- // ---------------------------------------------------------------------------
328
313
  // Persona grouping (internal)
329
- // ---------------------------------------------------------------------------
330
314
 
331
315
  function getPersonaGroups(findings) {
332
316
  const ref = getWcagReference();
@@ -383,9 +367,7 @@ function getPersonaGroups(findings) {
383
367
  return groups;
384
368
  }
385
369
 
386
- // ---------------------------------------------------------------------------
387
370
  // Audit summary
388
- // ---------------------------------------------------------------------------
389
371
 
390
372
  /**
391
373
  * Computes a complete audit summary from enriched findings: severity totals,
@@ -443,9 +425,7 @@ export function getOverview(findings, payload = null) {
443
425
  };
444
426
  }
445
427
 
446
- // ---------------------------------------------------------------------------
447
428
  // Knowledge APIs
448
- // ---------------------------------------------------------------------------
449
429
 
450
430
  /**
451
431
  * Returns scanner-facing help metadata including engine descriptions,
@@ -583,6 +563,13 @@ export function getSeverityLevels(options = {}) {
583
563
  };
584
564
  }
585
565
 
566
+ export const VIEWPORT_PRESETS = [
567
+ { label: "Desktop", width: 1280, height: 800 },
568
+ { label: "Laptop", width: 1440, height: 900 },
569
+ { label: "Tablet", width: 768, height: 1024 },
570
+ { label: "Mobile", width: 375, height: 812 },
571
+ ];
572
+
586
573
  export function getKnowledge(options = {}) {
587
574
  const scanner = getScannerHelp(options);
588
575
  const personas = getPersonaReference(options);
@@ -611,9 +598,7 @@ export function getKnowledge(options = {}) {
611
598
  };
612
599
  }
613
600
 
614
- // ---------------------------------------------------------------------------
615
601
  // Full audit pipeline
616
- // ---------------------------------------------------------------------------
617
602
 
618
603
  /**
619
604
  * Runs a complete accessibility audit: crawl + scan (axe + CDP + pa11y) + analyze.
@@ -818,9 +803,7 @@ export async function runAudit(options) {
818
803
  return findingsPayload;
819
804
  }
820
805
 
821
- // ---------------------------------------------------------------------------
822
806
  // Report generation
823
- // ---------------------------------------------------------------------------
824
807
 
825
808
  import {
826
809
  normalizeFindings as normalizeForReports,
@@ -1003,9 +986,7 @@ export async function getChecklist(options = {}) {
1003
986
  };
1004
987
  }
1005
988
 
1006
- // ---------------------------------------------------------------------------
1007
989
  // HTML Report
1008
- // ---------------------------------------------------------------------------
1009
990
 
1010
991
  /**
1011
992
  * Generates an interactive HTML audit dashboard from raw scan findings.
@@ -1135,9 +1116,7 @@ export async function getHTMLReport(payload, options = {}) {
1135
1116
  };
1136
1117
  }
1137
1118
 
1138
- // ---------------------------------------------------------------------------
1139
1119
  // Remediation Guide (Markdown)
1140
- // ---------------------------------------------------------------------------
1141
1120
 
1142
1121
  /**
1143
1122
  * Generates a Markdown remediation guide from raw scan findings.
@@ -1163,9 +1142,7 @@ export async function getRemediationGuide(payload, options = {}) {
1163
1142
  };
1164
1143
  }
1165
1144
 
1166
- // ---------------------------------------------------------------------------
1167
1145
  // Source Pattern Scanner
1168
- // ---------------------------------------------------------------------------
1169
1146
 
1170
1147
  /**
1171
1148
  * Scans a project's source code for accessibility patterns not detectable by axe-core.
@@ -935,17 +935,12 @@ function mergeViolations(axeViolations, cdpViolations, pa11yViolations) {
935
935
  }
936
936
  }
937
937
 
938
- // Step 3: pa11y findings — check via canonical rule ID (axe-equivalent) + selector
938
+ // Step 3: pa11y findings — only skip if same rule + same target already exists
939
939
  for (const v of pa11yViolations) {
940
940
  const target = v.nodes?.[0]?.target?.[0] || "";
941
941
  const key = `${v.id}::${target}`;
942
942
 
943
- // If pa11y was mapped to an axe rule ID, check if that rule already covers this target
944
- const isAxeEquivDuplicate = v.id && seenRuleTargets.has(v.id) && target && seenRuleTargets.get(v.id).has(target);
945
- // Also check if any existing finding covers this exact target (broader dedup)
946
- const selectorCovered = target && [...seen].some((k) => k.endsWith(`::${target}`));
947
-
948
- if (!seen.has(key) && !isAxeEquivDuplicate && (!selectorCovered || !target)) {
943
+ if (!seen.has(key)) {
949
944
  seen.add(key);
950
945
  if (!seenRuleTargets.has(v.id)) seenRuleTargets.set(v.id, new Set());
951
946
  seenRuleTargets.get(v.id).add(target);