@diegovelasquezweb/a11y-engine 0.9.0 → 0.10.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 CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] — 2026-03-16
9
+
10
+ ### Changed
11
+
12
+ - **Knowledge API consolidated** — `getScannerHelp`, `getPersonaReference`, `getUiHelp`, `getConformanceLevels`, `getWcagPrinciples`, and `getSeverityLevels` are no longer part of the public API. They remain as internal helpers consumed by `getKnowledge`. `getUiHelp` renamed to `getConceptsAndGlossary` internally. `getKnowledge` is the single exported entry point for all knowledge data.
13
+ - TypeScript declarations (`src/index.d.mts`) updated to remove the six individual knowledge functions.
14
+ - `tests/knowledge-api.test.mjs` updated to reflect the consolidated API shape.
15
+
16
+ ---
17
+
18
+ ## [0.8.5] — 2026-03-16
19
+
20
+ ### Fixed
21
+
22
+ - **pa11y merge no longer drops findings with shared selectors** — the merge step was discarding pa11y findings whenever any prior finding (from axe or CDP) targeted the same selector, regardless of rule. Now pa11y findings are only de-duplicated when the exact same `rule_id + selector` combination already exists.
23
+
24
+ ---
25
+
26
+ ## [0.8.4] — 2026-03-15
27
+
28
+ ### Added
29
+
30
+ - **`DEFAULT_AI_SYSTEM_PROMPT` exported** — the default Claude system prompt is now part of the public API, allowing consumers to read, log, or extend it.
31
+ - **`VIEWPORT_PRESETS` exported** — four ready-made viewport presets (`Desktop`, `Laptop`, `Tablet`, `Mobile`) exported from the package root for use in scanner UI option pickers.
32
+ - **`dependabot.yml`** — automated dependency update configuration added.
33
+ - **Effort fallback** — `getFindings` now infers `effort` after intelligence enrichment so findings that gain a `fixCode` from the intelligence database are correctly rated `"low"`.
34
+
35
+ ---
36
+
37
+ ## [0.8.3] — 2026-03-15
38
+
39
+ ### Fixed
40
+
41
+ - **`actual` field no longer contains axe preamble** — the `"Fix any of the following:"` prefix from axe `failureSummary` strings is now stripped in `analyzer.mjs`, producing a cleaner violation description.
42
+
43
+ ### Added
44
+
45
+ - `SECURITY.md` — security policy and vulnerability reporting process.
46
+
47
+ ---
48
+
8
49
  ## [0.8.2] — 2026-03-16
9
50
 
10
51
  ### Changed
package/README.md CHANGED
@@ -94,12 +94,18 @@ Computes severity totals, compliance score, WCAG pass/fail status, persona impac
94
94
  ```ts
95
95
  const summary = getOverview(findings, payload);
96
96
  // summary.score -> 72
97
- // summary.label -> "Good"
98
- // summary.wcagStatus -> "Fail"
97
+ // summary.label -> "Good" // "Excellent" | "Good" | "Fair" | "Poor" | "Critical"
98
+ // summary.wcagStatus -> "Fail" // "Pass" | "Conditional Pass" | "Fail"
99
99
  // summary.totals -> { Critical: 1, Serious: 3, Moderate: 5, Minor: 2 }
100
- // summary.personaGroups -> { screenReader: {...}, keyboard: {...}, ... }
101
- // summary.quickWins -> [top 3 fixable Critical/Serious findings]
100
+ // summary.personaGroups -> {
101
+ // screenReader: { label: "Screen Readers", count: 4, icon: "screenReader" },
102
+ // keyboard: { label: "Keyboard Only", count: 2, icon: "keyboard" },
103
+ // ...
104
+ // }
105
+ // summary.quickWins -> [top 3 Critical/Serious findings with fixCode]
106
+ // summary.targetUrl -> "https://example.com"
102
107
  // summary.detectedStack -> { framework: "nextjs", cms: null, uiLibraries: [] }
108
+ // summary.totalFindings -> 11
103
109
  ```
104
110
 
105
111
  ### Output API
@@ -136,24 +142,12 @@ See [API Reference](docs/api-reference.md) for the full `EngineKnowledge` shape.
136
142
 
137
143
  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.
138
144
 
139
- ## AI enrichment
140
-
141
- When `ANTHROPIC_API_KEY` is set, the engine runs a post-scan enrichment step that sends Critical and Serious findings to Claude. Claude generates:
142
-
143
- - A specific fix description referencing the actual selector, colors, and violation data
144
- - A production-quality code snippet in the correct framework syntax
145
- - Context-aware suggestions when repo source files are available
146
-
147
- 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`.
148
-
149
- The system prompt is fully customizable via `options.ai.systemPrompt` (programmatic API) or the `AI_SYSTEM_PROMPT` env var (CLI).
150
-
151
145
  ## Documentation
152
146
 
153
147
  | Resource | Description |
154
148
  | :--- | :--- |
155
149
  | [Architecture](docs/architecture.md) | Multi-engine pipeline, merge logic, and execution model |
156
- | [API Reference](docs/api-reference.md) | Function signatures, options, and return contracts |
150
+ | [API Reference](docs/api-reference.md) | Function signatures, options, return contracts, and exported constants |
157
151
  | [CLI Handbook](docs/cli-handbook.md) | Full flag reference and usage examples |
158
152
  | [Output Artifacts](docs/outputs.md) | Schema and structure of every generated file |
159
153
  | [Engine Manifest](docs/engine-manifest.md) | Current inventory of source modules, assets, and tests |
@@ -21,12 +21,9 @@
21
21
  - [getSourcePatterns](#getsourcepatternsprojdir-options)
22
22
  - [Knowledge API](#knowledge-api)
23
23
  - [getKnowledge](#getknowledgeoptions)
24
- - [getScannerHelp](#getscannerhelpoptions)
25
- - [getPersonaReference](#getpersonareferenceoptions)
26
- - [getUiHelp](#getuihelpoptions)
27
- - [getConformanceLevels](#getconformancelevelsoptions)
28
- - [getWcagPrinciples](#getwcagprinciplesoptions)
29
- - [getSeverityLevels](#getseveritylevelsoptions)
24
+ - [Constants](#constants)
25
+ - [VIEWPORT_PRESETS](#viewport_presets)
26
+ - [DEFAULT_AI_SYSTEM_PROMPT](#default_ai_system_prompt)
30
27
 
31
28
  ---
32
29
 
@@ -53,12 +50,8 @@ import {
53
50
  getRemediationGuide,
54
51
  getSourcePatterns,
55
52
  getKnowledge,
56
- getScannerHelp,
57
- getPersonaReference,
58
- getUiHelp,
59
- getConformanceLevels,
60
- getWcagPrinciples,
61
- getSeverityLevels,
53
+ VIEWPORT_PRESETS,
54
+ DEFAULT_AI_SYSTEM_PROMPT,
62
55
  } from "@diegovelasquezweb/a11y-engine";
63
56
  ```
64
57
 
@@ -81,9 +74,9 @@ const payload = await runAudit({
81
74
  const findings = getFindings(payload);
82
75
 
83
76
  // 3. Get compliance summary
84
- const { score, scoreLabel, wcagStatus, totals, quickWins } = getOverview(findings, payload);
77
+ const { score, label, wcagStatus, totals, quickWins } = getOverview(findings, payload);
85
78
 
86
- console.log(`Score: ${score}/100 (${scoreLabel})`);
79
+ console.log(`Score: ${score}/100 (${label})`);
87
80
  console.log(`WCAG Status: ${wcagStatus}`);
88
81
  console.log(`Critical: ${totals.Critical}, Serious: ${totals.Serious}`);
89
82
  ```
@@ -169,6 +162,42 @@ const payload = await runAudit({
169
162
 
170
163
  Returns: `Promise<ScanPayload>`
171
164
 
165
+ **`ScanPayload` shape:**
166
+
167
+ ```ts
168
+ {
169
+ findings: RawFinding[], // Raw findings from axe/CDP/pa11y merge
170
+ metadata: {
171
+ target_url: string, // The baseUrl that was scanned
172
+ scanned_at: string, // ISO 8601 timestamp
173
+ engines: { // Which engines actually ran
174
+ axe: boolean,
175
+ cdp: boolean,
176
+ pa11y: boolean,
177
+ },
178
+ projectContext: { // Auto-detected or overridden stack
179
+ framework: string | null, // "nextjs" | "react" | "vue" | etc.
180
+ cms: string | null, // "wordpress" | "shopify" | etc.
181
+ uiLibraries: string[], // ["radix-ui", "tailwindcss", ...]
182
+ },
183
+ routes_scanned: number, // How many pages were actually scanned
184
+ discovery_method: string, // "crawl" | "explicit"
185
+ },
186
+ incomplete_findings?: RawFinding[], // axe "incomplete" results (needs-review)
187
+ patternFindings?: { // Only present if projectDir/repoUrl + !skipPatterns
188
+ generated_at: string,
189
+ project_dir: string, // Local path or repo URL
190
+ findings: SourcePatternFinding[],
191
+ summary: {
192
+ total: number,
193
+ confirmed: number,
194
+ potential: number,
195
+ },
196
+ },
197
+ ai_enriched_findings?: EnrichedFinding[], // Only present if ai.enabled + ai.apiKey
198
+ }
199
+ ```
200
+
172
201
  > **`ai_enriched_findings` fast path**: When AI enrichment runs, `getFindings()` uses `payload.ai_enriched_findings` directly instead of re-normalizing the raw findings array.
173
202
 
174
203
  ---
@@ -207,6 +236,85 @@ const findings = getFindings(payload, {
207
236
 
208
237
  Returns: `EnrichedFinding[]`
209
238
 
239
+ **`EnrichedFinding` shape:**
240
+
241
+ ```ts
242
+ {
243
+ // Identity
244
+ id: string, // "A11Y-001", "A11Y-002", ...
245
+ ruleId: string, // Canonical rule ID (e.g. "color-contrast")
246
+ source: string, // "axe" | "cdp" | "pa11y"
247
+ sourceRuleId: string | null, // Original engine rule ID before canonicalization
248
+
249
+ // Classification
250
+ title: string, // Human-readable issue title
251
+ severity: string, // "Critical" | "Serious" | "Moderate" | "Minor"
252
+ category: string | null, // "color", "forms", "structure", "aria", ...
253
+ wcag: string, // WCAG criterion (e.g. "1.4.3")
254
+ wcagCriterionId: string | null, // Full criterion ID (e.g. "1.4.3")
255
+ wcagClassification: string | null, // "A" | "AA" | "AAA" | "Best Practice"
256
+
257
+ // Location
258
+ area: string, // Page path (e.g. "/about")
259
+ url: string, // Full page URL
260
+ selector: string, // CSS selector of the violating element
261
+ primarySelector: string, // Preferred selector for targeting
262
+
263
+ // Problem description
264
+ actual: string, // What the engine found
265
+ expected: string, // What WCAG requires
266
+ impactedUsers: string, // "Screen reader users", "Keyboard users", etc.
267
+ primaryFailureMode: string | null, // "missing-label" | "low-contrast" | ...
268
+ relationshipHint: string | null, // How this relates to other findings
269
+
270
+ // Evidence
271
+ evidence: object[], // Raw evidence from the engine
272
+ failureChecks: object[], // axe check details
273
+ relatedContext: object[], // Related DOM elements
274
+ totalInstances: number | null, // How many elements are affected
275
+ pagesAffected: number | null, // How many pages have this issue
276
+ affectedUrls: string[] | null, // Specific URLs affected
277
+
278
+ // Fix guidance
279
+ fixDescription: string | null, // Human-readable fix explanation
280
+ fixCode: string | null, // Code snippet to fix the issue
281
+ fixCodeLang: string, // "html" | "css" | "jsx" | ...
282
+ recommendedFix: string, // Short fix summary
283
+ mdn: string | null, // MDN reference URL
284
+ effort: string, // "low" (has fixCode) | "high" (no fixCode)
285
+ fixDifficultyNotes: object | null, // Detailed difficulty breakdown
286
+
287
+ // Framework / CMS context
288
+ frameworkNotes: string | null, // Framework-specific fix guidance
289
+ cmsNotes: string | null, // CMS-specific fix guidance
290
+ managedByLibrary: string | null, // If the element is from a 3rd-party lib
291
+ componentHint: string | null, // Likely component name
292
+ fileSearchPattern: string | null, // Glob pattern to find source file
293
+
294
+ // Ownership & search
295
+ ownershipStatus: string, // "own" | "third-party" | "unknown"
296
+ ownershipReason: string | null, // Why it was classified that way
297
+ primarySourceScope: string[], // Directories to search for source
298
+ searchStrategy: string, // "verify_ownership_before_search" | ...
299
+
300
+ // Verification
301
+ verificationCommand: string | null, // CLI command to verify the fix
302
+ verificationCommandFallback: string | null,
303
+ screenshotPath: string | null, // Path or URL to element screenshot
304
+
305
+ // Metadata
306
+ relatedRules: string[], // Related axe rule IDs
307
+ falsePositiveRisk: string | null, // "low" | "medium" | "high"
308
+ guardrails: object | null, // Guardrail metadata from the engine
309
+ checkData: object | null, // Raw check data from the engine
310
+
311
+ // AI enrichment (only when ai.enabled ran)
312
+ aiEnhanced?: boolean, // true when AI enriched this finding
313
+ aiFixDescription?: string, // Claude-generated fix explanation
314
+ aiFixCode?: string, // Claude-generated code snippet
315
+ }
316
+ ```
317
+
210
318
  ---
211
319
 
212
320
  ### `getOverview(findings, payload?)`
@@ -218,27 +326,39 @@ import { getFindings, getOverview } from "@diegovelasquezweb/a11y-engine";
218
326
 
219
327
  const findings = getFindings(payload);
220
328
  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
329
  ```
239
330
 
240
331
  Returns: `AuditSummary`
241
332
 
333
+ **`AuditSummary` shape:**
334
+
335
+ ```ts
336
+ {
337
+ score: number, // 0–100. Formula: 100 - (Critical×15) - (Serious×5) - (Moderate×2) - (Minor×0.5)
338
+ label: string, // "Excellent" (90–100) | "Good" (75–89) | "Fair" (55–74) | "Poor" (35–54) | "Critical" (0–34)
339
+ wcagStatus: string, // "Pass" | "Conditional Pass" | "Fail"
340
+ totals: {
341
+ Critical: number,
342
+ Serious: number,
343
+ Moderate: number,
344
+ Minor: number,
345
+ },
346
+ personaGroups: Record<string, { // Keyed by persona ID
347
+ label: string, // "Screen Readers", "Keyboard Only", ...
348
+ count: number, // Findings affecting this persona
349
+ icon: string, // Same as persona ID
350
+ }>,
351
+ quickWins: EnrichedFinding[], // Top 3 Critical/Serious findings with fixCode
352
+ targetUrl: string, // The scanned URL
353
+ detectedStack: {
354
+ framework: string | null, // "nextjs" | "react" | etc.
355
+ cms: string | null, // "wordpress" | "shopify" | etc.
356
+ uiLibraries: string[], // ["radix-ui", "tailwindcss", ...]
357
+ },
358
+ totalFindings: number, // Total enriched findings count
359
+ }
360
+ ```
361
+
242
362
  ---
243
363
 
244
364
  ## Output API
@@ -254,12 +374,18 @@ const { buffer, contentType } = await getPDFReport(payload, {
254
374
  baseUrl: "https://example.com",
255
375
  target: "WCAG 2.2 AA",
256
376
  });
257
-
258
- // In a Next.js API route:
259
- return new Response(buffer, { headers: { "Content-Type": contentType } });
260
377
  ```
261
378
 
262
- Returns: `Promise<{ buffer: Buffer, contentType: string }>`
379
+ Returns: `Promise<PDFReportResult>`
380
+
381
+ **`PDFReportResult` shape:**
382
+
383
+ ```ts
384
+ {
385
+ buffer: Buffer, // Raw PDF binary data
386
+ contentType: "application/pdf", // MIME type for response headers
387
+ }
388
+ ```
263
389
 
264
390
  ---
265
391
 
@@ -276,7 +402,16 @@ const { html, contentType } = await getHTMLReport(payload, {
276
402
  });
277
403
  ```
278
404
 
279
- Returns: `Promise<{ html: string, contentType: string }>`
405
+ Returns: `Promise<HTMLReportResult>`
406
+
407
+ **`HTMLReportResult` shape:**
408
+
409
+ ```ts
410
+ {
411
+ html: string, // Self-contained HTML document string
412
+ contentType: "text/html", // MIME type for response headers
413
+ }
414
+ ```
280
415
 
281
416
  ---
282
417
 
@@ -292,7 +427,16 @@ const { html, contentType } = await getChecklist({
292
427
  });
293
428
  ```
294
429
 
295
- Returns: `Promise<{ html: string, contentType: string }>`
430
+ Returns: `Promise<ChecklistResult>`
431
+
432
+ **`ChecklistResult` shape:**
433
+
434
+ ```ts
435
+ {
436
+ html: string, // Self-contained HTML with interactive checklist
437
+ contentType: "text/html", // MIME type for response headers
438
+ }
439
+ ```
296
440
 
297
441
  ---
298
442
 
@@ -307,11 +451,18 @@ const { markdown, contentType } = await getRemediationGuide(payload, {
307
451
  baseUrl: "https://example.com",
308
452
  patternFindings: payload.patternFindings ?? null,
309
453
  });
310
-
311
- // Write to disk or return as download
312
454
  ```
313
455
 
314
- Returns: `Promise<{ markdown: string, contentType: string }>`
456
+ Returns: `Promise<RemediationGuideResult>`
457
+
458
+ **`RemediationGuideResult` shape:**
459
+
460
+ ```ts
461
+ {
462
+ markdown: string, // Full Markdown document with remediation roadmap
463
+ contentType: "text/markdown", // MIME type for response headers
464
+ }
465
+ ```
315
466
 
316
467
  ---
317
468
 
@@ -326,29 +477,34 @@ const result = await getSourcePatterns("./", {
326
477
  framework: "nextjs", // optional — scopes scan to framework source dirs
327
478
  onlyPattern: "placeholder-only-label", // optional — run a single pattern
328
479
  });
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
480
  ```
349
481
 
350
482
  Returns: `Promise<SourcePatternResult>`
351
483
 
484
+ **`SourcePatternResult` shape:**
485
+
486
+ ```ts
487
+ {
488
+ findings: {
489
+ id: string, // "PAT-a1b2c3" — unique pattern finding ID
490
+ pattern_id: string, // Pattern definition ID (e.g. "placeholder-only-label")
491
+ title: string, // Human-readable issue title
492
+ severity: string, // "Critical" | "Serious" | "Moderate" | "Minor"
493
+ status: string, // "confirmed" | "potential"
494
+ file: string, // Relative file path (e.g. "src/components/SearchBar.tsx")
495
+ line: number, // Line number where the pattern was found
496
+ match: string, // The matching source code line
497
+ context: string, // Surrounding code for context
498
+ fix_description: string, // How to fix the pattern
499
+ }[],
500
+ summary: {
501
+ total: number, // Total findings found
502
+ confirmed: number, // Definite accessibility issues
503
+ potential: number, // Likely issues that need manual review
504
+ },
505
+ }
506
+ ```
507
+
352
508
  ---
353
509
 
354
510
  ## Knowledge API
@@ -357,6 +513,8 @@ Returns: `Promise<SourcePatternResult>`
357
513
 
358
514
  Returns all accessibility knowledge in a single call. Accepts an optional `{ locale?: string }` option (default: `"en"`).
359
515
 
516
+ This is the **only exported Knowledge API function**. The data it returns covers scanner help, persona profiles, concepts, glossary, docs, conformance levels, WCAG principles, and severity definitions — all in one call.
517
+
360
518
  ```ts
361
519
  import { getKnowledge } from "@diegovelasquezweb/a11y-engine";
362
520
 
@@ -365,16 +523,135 @@ const knowledge = getKnowledge({ locale: "en" });
365
523
 
366
524
  **Returns:** `EngineKnowledge`
367
525
 
368
- | Field | Type | Description |
369
- | :--- | :--- | :--- |
370
- | `scanner` | `{ title, engines, options }` | Scan option descriptions, allowed values, and engine metadata |
371
- | `personas` | `PersonaReferenceItem[]` | Persona labels, icons, descriptions, and mapped rules |
372
- | `concepts` | `Record<string, ConceptEntry>` | Concept definitions with title, body, and context |
373
- | `glossary` | `GlossaryEntry[]` | Accessibility term definitions |
374
- | `docs` | `KnowledgeDocs` | Documentation articles organized by section and group |
375
- | `conformanceLevels` | `ConformanceLevel[]` | WCAG A/AA/AAA definitions with axe-core tag mappings |
376
- | `wcagPrinciples` | `WcagPrinciple[]` | The four WCAG principles with criterion prefix patterns |
377
- | `severityLevels` | `SeverityLevel[]` | Critical/Serious/Moderate/Minor definitions with ordering |
526
+ **`EngineKnowledge` shape:**
527
+
528
+ ```ts
529
+ {
530
+ locale: string, // "en"
531
+ version: string, // "1.0.0"
532
+
533
+ scanner: {
534
+ title: string, // "Scanner Help"
535
+ engines: { // Engine descriptions
536
+ id: string, // "axe" | "cdp" | "pa11y"
537
+ label: string,
538
+ description: string,
539
+ }[],
540
+ options: { // CLI/API option descriptions
541
+ name: string, // "maxRoutes"
542
+ type: string, // "number" | "string" | "boolean"
543
+ default: string | number | boolean,
544
+ description: string,
545
+ values?: string[], // Allowed values if enum-like
546
+ }[],
547
+ },
548
+
549
+ personas: { // Disability persona profiles
550
+ id: string, // "screenReader" | "keyboard" | "vision" | "cognitive"
551
+ icon: string, // Same as id, used for icon lookup
552
+ label: string, // "Screen Readers"
553
+ description: string, // Explanation of the persona
554
+ keywords: string[], // Keywords for matching findings
555
+ mappedRules: string[], // axe rule IDs mapped to this persona
556
+ }[],
557
+
558
+ concepts: Record<string, { // Concept definitions keyed by ID
559
+ title: string,
560
+ body: string,
561
+ context?: string, // When/where this concept applies
562
+ }>,
563
+
564
+ glossary: { // Accessibility term definitions
565
+ term: string,
566
+ definition: string,
567
+ }[],
568
+
569
+ docs: { // Documentation articles
570
+ sections: {
571
+ id: string,
572
+ title: string,
573
+ groups: {
574
+ id: string,
575
+ title: string,
576
+ articles: {
577
+ id: string,
578
+ title: string,
579
+ body: string,
580
+ }[],
581
+ }[],
582
+ }[],
583
+ },
584
+
585
+ conformanceLevels: { // WCAG A/AA/AAA definitions
586
+ level: string, // "A" | "AA" | "AAA"
587
+ label: string,
588
+ description: string,
589
+ axeTags: string[], // ["wcag2a", "wcag21a", "wcag22a"]
590
+ }[],
591
+
592
+ wcagPrinciples: { // The four WCAG principles
593
+ id: string, // "perceivable" | "operable" | "understandable" | "robust"
594
+ label: string,
595
+ description: string,
596
+ criterionPrefix: string, // "1." | "2." | "3." | "4."
597
+ }[],
598
+
599
+ severityLevels: { // Severity definitions
600
+ level: string, // "Critical" | "Serious" | "Moderate" | "Minor"
601
+ label: string,
602
+ description: string,
603
+ order: number, // 1 (Critical) – 4 (Minor)
604
+ }[],
605
+ }
606
+ ```
607
+
608
+ ---
609
+
610
+ ## Constants
611
+
612
+ ### `VIEWPORT_PRESETS`
613
+
614
+ Ready-made viewport dimensions for common device classes. Useful when building scanner UI option pickers.
615
+
616
+ ```ts
617
+ import { VIEWPORT_PRESETS } from "@diegovelasquezweb/a11y-engine";
618
+
619
+ // VIEWPORT_PRESETS:
620
+ // [
621
+ // { label: "Desktop", width: 1280, height: 800 },
622
+ // { label: "Laptop", width: 1440, height: 900 },
623
+ // { label: "Tablet", width: 768, height: 1024 },
624
+ // { label: "Mobile", width: 375, height: 812 },
625
+ // ]
626
+ ```
627
+
628
+ Type: `ViewportPreset[]` — `{ label: string; width: number; height: number }[]`
629
+
630
+ ---
631
+
632
+ ### `DEFAULT_AI_SYSTEM_PROMPT`
633
+
634
+ The default system prompt passed to Claude for AI enrichment. Exported so consumers can read, log, or extend it when building custom AI workflows.
635
+
636
+ ```ts
637
+ import { DEFAULT_AI_SYSTEM_PROMPT } from "@diegovelasquezweb/a11y-engine";
638
+
639
+ // Override for a specific scan:
640
+ await runAudit({
641
+ baseUrl: "https://example.com",
642
+ ai: {
643
+ enabled: true,
644
+ apiKey: process.env.ANTHROPIC_API_KEY,
645
+ systemPrompt: DEFAULT_AI_SYSTEM_PROMPT + "\n\nFocus on Vue 3 Composition API patterns.",
646
+ },
647
+ });
648
+ ```
649
+
650
+ Type: `string`
651
+
652
+ ---
653
+
654
+ > **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.
378
655
 
379
656
  ---
380
657
 
@@ -119,7 +119,7 @@ Output shape:
119
119
 
120
120
  Core artifacts generated by the pipeline:
121
121
 
122
- - `progress.json`: step status (`page`, `axe`, `cdp`, `pa11y`, `merge`, `intelligence`)
122
+ - `progress.json`: step status `page`, `axe`, `cdp`, `pa11y`, `merge`, `intelligence`, `repo` (remote package.json fetch), `patterns` (source pattern scan), `ai` (Claude enrichment)
123
123
  - `a11y-scan-results.json`: merged runtime scan output per route
124
124
  - `a11y-findings.json`: enriched findings payload used by reports and API consumers
125
125
 
@@ -49,6 +49,7 @@ This document is the current technical inventory of the engine package.
49
49
  | `assets/remediation/code-patterns.mjs` | Source code pattern definitions |
50
50
  | `assets/remediation/source-boundaries.mjs` | Framework source boundaries |
51
51
  | `assets/remediation/axe-check-maps.mjs` | axe check-to-rule mappings |
52
+ | `assets/knowledge/knowledge.mjs` | Knowledge API data — scanner help, personas, concepts, glossary, docs, conformance levels, WCAG principles, severity definitions |
52
53
  | `assets/reporting/compliance-config.mjs` | Compliance scoring configuration |
53
54
  | `assets/reporting/wcag-reference.mjs` | WCAG + persona mapping reference |
54
55
  | `assets/reporting/manual-checks.mjs` | Manual checklist data |
@@ -62,6 +63,7 @@ Current files:
62
63
  - `tests/asset-loader.test.mjs`
63
64
  - `tests/audit-summary.test.mjs`
64
65
  - `tests/enriched-findings.test.mjs`
66
+ - `tests/knowledge-api.test.mjs`
65
67
  - `tests/reports-api.test.mjs`
66
68
  - `tests/reports-paths.test.mjs`
67
69
  - `tests/run-audit.integration.test.mjs`
@@ -1,6 +1,6 @@
1
1
  # Intelligence & Enrichment
2
2
 
3
- **Navigation**: [Home](../README.md) • [Architecture](architecture.md) • [API Reference](api-reference.md) • [CLI Handbook](cli-handbook.md) • [Output Artifacts](outputs.md) • [Engine Manifest](engine-manifest.md) • [Intelligence](intelligence.md) • [Testing](testing.md)
3
+ **Navigation**: [Home](../README.md) • [Architecture](architecture.md) • [Intelligence](intelligence.md) • [API Reference](api-reference.md) • [CLI Handbook](cli-handbook.md) • [Output Artifacts](outputs.md) • [Engine Manifest](engine-manifest.md) • [Testing](testing.md)
4
4
 
5
5
  ---
6
6
 
package/docs/outputs.md CHANGED
@@ -63,6 +63,10 @@ Real-time scan progress written by `src/pipeline/dom-scanner.mjs` as each engine
63
63
  | `cdp` | CDP | Chrome DevTools Protocol accessibility tree check. `found` = issue count. |
64
64
  | `pa11y` | pa11y | HTML CodeSniffer scan. `found` = issue count. |
65
65
  | `merge` | — | Cross-engine merge and deduplication. `merged` = final unique count. |
66
+ | `repo` | — | Remote `package.json` fetch via GitHub API for stack detection. Only emitted when `repoUrl` is set. |
67
+ | `intelligence` | — | Analyzer enrichment step (fix intelligence, ownership, scoring). |
68
+ | `patterns` | — | Source code pattern scan (local or remote). Only emitted when `projectDir` or `repoUrl` is set. `total` / `confirmed` / `potential` counts in `extra`. |
69
+ | `ai` | Claude | AI enrichment via Anthropic API. Only emitted when `ai.enabled` is `true` and `ai.apiKey` is set. |
66
70
 
67
71
  ### Step statuses
68
72
 
package/docs/testing.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  - **Framework**: Vitest
16
16
  - **Command**: `pnpm test`
17
- - **Current suite**: 8 files, 26 tests
17
+ - **Current suite**: 9 files, 38 tests
18
18
 
19
19
  The suite focuses on regression protection for architecture changes, public API contracts, and critical report-generation paths.
20
20
 
@@ -32,19 +32,24 @@ The suite focuses on regression protection for architecture changes, public API
32
32
  - `tests/audit-summary.test.mjs`
33
33
  - Verifies canonicalization, normalization, sorting, effort inference, quick wins, and detected stack output.
34
34
 
35
- ### 3) Report API and import safety
35
+ ### 3) Knowledge API
36
+
37
+ - `tests/knowledge-api.test.mjs`
38
+ - Verifies `getKnowledge()` returns the full expected shape: scanner, personas, concepts, glossary, docs, conformance levels, WCAG principles, and severity levels.
39
+
40
+ ### 4) Report API and import safety
36
41
 
37
42
  - `tests/reports-api.test.mjs`
38
43
  - `tests/reports-paths.test.mjs`
39
44
  - Verifies report APIs return expected output types and protects against broken relative imports after refactors.
40
45
 
41
- ### 4) Source-pattern behavior
46
+ ### 5) Source-pattern behavior
42
47
 
43
48
  - `tests/source-patterns.test.mjs`
44
49
  - `tests/source-scanner-utils.test.mjs`
45
50
  - Verifies edge behavior for pattern filtering and source scanner utility functions.
46
51
 
47
- ### 5) Integration tests (no network)
52
+ ### 6) Integration tests (no network)
48
53
 
49
54
  - `tests/run-audit.integration.test.mjs`
50
55
  - Mocks scanner/analyzer modules to verify:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/audit.mjs CHANGED
@@ -34,6 +34,7 @@ Audit Intelligence:
34
34
  --axe-tags <csv> Comma-separated axe tags (e.g., wcag2a,wcag2aa).
35
35
  --only-rule <id> Only check for this specific rule ID.
36
36
  --ignore-findings <csv> Ignore specific rule IDs.
37
+ --include-incomplete Include axe "incomplete" items as findings.
37
38
  --exclude-selectors <csv> Exclude CSS selectors from scan.
38
39
 
39
40
  Execution & Emulation:
@@ -187,6 +188,7 @@ async function main() {
187
188
  const skipPatterns = argv.includes("--skip-patterns");
188
189
  const affectedOnly = argv.includes("--affected-only");
189
190
  const ignoreFindings = getArgValue("ignore-findings");
191
+ const includeIncomplete = argv.includes("--include-incomplete");
190
192
  const excludeSelectors = getArgValue("exclude-selectors");
191
193
 
192
194
  const waitUntil = getArgValue("wait-until");
@@ -290,6 +292,7 @@ async function main() {
290
292
 
291
293
  const analyzerArgs = [];
292
294
  if (ignoreFindings) analyzerArgs.push("--ignore-findings", ignoreFindings);
295
+ if (includeIncomplete) analyzerArgs.push("--include-incomplete");
293
296
  const resolvedFramework = framework || detectedFramework;
294
297
  if (resolvedFramework) analyzerArgs.push("--framework", resolvedFramework);
295
298
  await runScript("../enrichment/analyzer.mjs", analyzerArgs);
@@ -257,6 +257,7 @@ function printUsage() {
257
257
  Options:
258
258
  --output <path> Output findings JSON path (default: .audit/a11y-findings.json)
259
259
  --ignore-findings <csv> Ignore specific rule IDs (overrides config)
260
+ --include-incomplete Include axe "incomplete" items as findings
260
261
  -h, --help Show this help
261
262
  `);
262
263
  }
@@ -276,10 +277,15 @@ function parseArgs(argv) {
276
277
  input: getInternalPath("a11y-scan-results.json"),
277
278
  output: getInternalPath("a11y-findings.json"),
278
279
  ignoreFindings: [],
280
+ includeIncomplete: false,
279
281
  };
280
282
 
281
283
  for (let i = 0; i < argv.length; i += 1) {
282
284
  const key = argv[i];
285
+ if (key === "--include-incomplete") {
286
+ args.includeIncomplete = true;
287
+ continue;
288
+ }
283
289
  const value = argv[i + 1];
284
290
  if (!key.startsWith("--") || value === undefined) continue;
285
291
 
@@ -541,6 +547,7 @@ export function classifyFindingOwnership({
541
547
  * @returns {boolean}
542
548
  */
543
549
  function isFalsePositive(finding) {
550
+ if (finding.needs_verification) return false;
544
551
  const htmls = finding.evidence.map((e) => e.html || "").join(" ");
545
552
  if (finding.rule_id === "color-contrast") {
546
553
  if (/background(?:-image)?\s*:\s*(?:linear|radial|conic)-gradient/i.test(htmls)) return true;
@@ -897,6 +904,7 @@ function buildFindings(inputPayload, cliArgs) {
897
904
  primary_source_scope: ownership.primarySourceScope,
898
905
  search_strategy: ownership.searchStrategy,
899
906
  component_hint: extractComponentHint(bestSelector) ?? derivePageHint(route.path),
907
+ needs_verification: !!v._fromIncomplete,
900
908
  verification_command: `pnpm a11y --base-url ${route.url} --routes ${route.path} --only-rule ${v.id} --max-routes 1`,
901
909
  verification_command_fallback: `node scripts/audit.mjs --base-url ${route.url} --routes ${route.path} --only-rule ${v.id} --max-routes 1`,
902
910
  });
@@ -966,12 +974,33 @@ export function collectIncompleteFindings(routes) {
966
974
  /**
967
975
  * Runs the analyzer programmatically on a scan payload.
968
976
  * @param {Object} scanPayload - The raw scan output from dom-scanner ({ routes, base_url, projectContext, ... }).
969
- * @param {{ ignoreFindings?: string[], framework?: string, output?: string }} [options={}]
977
+ * @param {{ ignoreFindings?: string[], framework?: string, output?: string, includeIncomplete?: boolean }} [options={}]
970
978
  * @returns {Object} The enriched findings payload { findings, incomplete_findings, metadata, ... }.
971
979
  */
972
980
  export function runAnalyzer(scanPayload, options = {}) {
973
981
  if (!scanPayload) throw new Error("Missing scan payload");
974
982
 
983
+ const sourceRoutes = scanPayload.routes || [];
984
+ const routesForAnalysis = options.includeIncomplete
985
+ ? sourceRoutes.map((route) => {
986
+ const incomplete = Array.isArray(route.incomplete) ? route.incomplete : [];
987
+ const violations = Array.isArray(route.violations) ? route.violations : [];
988
+ if (incomplete.length === 0) return route;
989
+ return {
990
+ ...route,
991
+ violations: [
992
+ ...violations,
993
+ ...incomplete.map((item) => ({ ...item, _fromIncomplete: true })),
994
+ ],
995
+ };
996
+ })
997
+ : sourceRoutes;
998
+
999
+ const scanPayloadForAnalysis =
1000
+ routesForAnalysis === sourceRoutes
1001
+ ? scanPayload
1002
+ : { ...scanPayload, routes: routesForAnalysis };
1003
+
975
1004
  const args = {
976
1005
  input: null,
977
1006
  output: options.output || getInternalPath("a11y-findings.json"),
@@ -980,7 +1009,7 @@ export function runAnalyzer(scanPayload, options = {}) {
980
1009
  };
981
1010
 
982
1011
  const ignoredRules = new Set(args.ignoreFindings);
983
- const result = buildFindings(scanPayload, args);
1012
+ const result = buildFindings(scanPayloadForAnalysis, args);
984
1013
 
985
1014
  if (ignoredRules.size > 0) {
986
1015
  const knownIds = new Set(
@@ -1056,6 +1085,7 @@ function main() {
1056
1085
  runAnalyzer(payload, {
1057
1086
  ignoreFindings: args.ignoreFindings,
1058
1087
  framework: args.framework,
1088
+ includeIncomplete: args.includeIncomplete,
1059
1089
  output: args.output,
1060
1090
  });
1061
1091
  }
package/src/index.d.mts CHANGED
@@ -51,6 +51,7 @@ export interface Finding {
51
51
  source_rule_id?: string | null;
52
52
  pages_affected?: number | null;
53
53
  affected_urls?: string[] | null;
54
+ needs_verification?: boolean;
54
55
  }
55
56
 
56
57
  export interface EnrichedFinding {
@@ -102,6 +103,7 @@ export interface EnrichedFinding {
102
103
  checkData: Record<string, unknown> | null;
103
104
  pagesAffected: number | null;
104
105
  affectedUrls: string[] | null;
106
+ needsVerification?: boolean;
105
107
  }
106
108
 
107
109
  export interface SeverityTotals {
@@ -142,6 +144,7 @@ export interface AuditSummary {
142
144
  export interface ScanPayload {
143
145
  findings: Finding[] | Record<string, unknown>[];
144
146
  metadata?: Record<string, unknown>;
147
+ incomplete_findings?: unknown[];
145
148
  }
146
149
 
147
150
  export interface ReportOptions {
@@ -397,6 +400,7 @@ export interface RunAuditOptions {
397
400
  repoUrl?: string;
398
401
  githubToken?: string;
399
402
  skipPatterns?: boolean;
403
+ includeIncomplete?: boolean;
400
404
  screenshotsDir?: string;
401
405
  engines?: EngineSelection;
402
406
  ai?: AiOptions;
package/src/index.mjs CHANGED
@@ -161,6 +161,7 @@ function normalizeSingleFinding(item, index, screenshotUrlBuilder) {
161
161
  check_data: item.check_data && typeof item.check_data === "object" ? item.check_data : null,
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
+ needs_verification: Boolean(item.needs_verification),
164
165
  };
165
166
  }
166
167
 
@@ -247,6 +248,7 @@ export function getFindings(input, options = {}) {
247
248
  checkData: finding.check_data,
248
249
  pagesAffected: finding.pages_affected,
249
250
  affectedUrls: finding.affected_urls,
251
+ needsVerification: finding.needs_verification,
250
252
  };
251
253
 
252
254
  // Enrich from intelligence if no fix data exists yet
@@ -379,8 +381,9 @@ function getPersonaGroups(findings) {
379
381
  * @returns {object} Full audit summary.
380
382
  */
381
383
  export function getOverview(findings, payload = null) {
384
+ const scorableFindings = findings.filter((f) => !f.needsVerification);
382
385
  const totals = { Critical: 0, Serious: 0, Moderate: 0, Minor: 0 };
383
- for (const f of findings) {
386
+ for (const f of scorableFindings) {
384
387
  const severity = f.severity || "";
385
388
  if (severity in totals) totals[severity] += 1;
386
389
  }
@@ -622,6 +625,7 @@ export function getKnowledge(options = {}) {
622
625
  * framework?: string,
623
626
  * projectDir?: string,
624
627
  * skipPatterns?: boolean,
628
+ * includeIncomplete?: boolean,
625
629
  * engines?: { axe?: boolean, cdp?: boolean, pa11y?: boolean },
626
630
  * onProgress?: (step: string, status: string, extra?: object) => void,
627
631
  * }} options
@@ -691,6 +695,7 @@ export async function runAudit(options) {
691
695
  const findingsPayload = runAnalyzer(scanPayload, {
692
696
  ignoreFindings: options.ignoreFindings,
693
697
  framework: options.framework,
698
+ includeIncomplete: options.includeIncomplete,
694
699
  });
695
700
 
696
701
  // Step 3: Source patterns (optional) — works with local projectDir or remote repoUrl