@diegovelasquezweb/a11y-engine 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -949,17 +949,23 @@ export function collectIncompleteFindings(routes) {
949
949
  }
950
950
 
951
951
  /**
952
- * The main execution function for the analyzer script.
953
- * Reads scan results, processes findings, and writes the final findings JSON.
952
+ * Runs the analyzer programmatically on a scan payload.
953
+ * @param {Object} scanPayload - The raw scan output from dom-scanner ({ routes, base_url, projectContext, ... }).
954
+ * @param {{ ignoreFindings?: string[], framework?: string, output?: string }} [options={}]
955
+ * @returns {Object} The enriched findings payload { findings, incomplete_findings, metadata, ... }.
954
956
  */
955
- function main() {
956
- const args = parseArgs(process.argv.slice(2));
957
- const ignoredRules = new Set(args.ignoreFindings);
957
+ export function runAnalyzer(scanPayload, options = {}) {
958
+ if (!scanPayload) throw new Error("Missing scan payload");
958
959
 
959
- const payload = readJson(args.input);
960
- if (!payload) throw new Error(`Input not found or invalid: ${args.input}`);
960
+ const args = {
961
+ input: null,
962
+ output: options.output || getInternalPath("a11y-findings.json"),
963
+ ignoreFindings: options.ignoreFindings || [],
964
+ framework: options.framework || null,
965
+ };
961
966
 
962
- const result = buildFindings(payload, args);
967
+ const ignoredRules = new Set(args.ignoreFindings);
968
+ const result = buildFindings(scanPayload, args);
963
969
 
964
970
  if (ignoredRules.size > 0) {
965
971
  const knownIds = new Set(
@@ -989,15 +995,15 @@ function main() {
989
995
  if (deduplicatedCount > 0) log.info(`Deduplicated ${deduplicatedCount} cross-page finding group(s).`);
990
996
 
991
997
  const overallAssessment = computeOverallAssessment(dedupedFindings);
992
- const passedCriteria = computePassedCriteria(payload.routes || [], WCAG_CRITERION_MAP, dedupedFindings);
993
- const outOfScope = computeOutOfScope(payload.routes || []);
998
+ const passedCriteria = computePassedCriteria(scanPayload.routes || [], WCAG_CRITERION_MAP, dedupedFindings);
999
+ const outOfScope = computeOutOfScope(scanPayload.routes || []);
994
1000
  const recommendations = computeRecommendations(dedupedFindings);
995
- const testingMethodology = computeTestingMethodology(payload);
996
- const incompleteFindings = collectIncompleteFindings(payload.routes || []);
1001
+ const testingMethodology = computeTestingMethodology(scanPayload);
1002
+ const incompleteFindings = collectIncompleteFindings(scanPayload.routes || []);
997
1003
  if (incompleteFindings.length > 0)
998
1004
  log.info(`${incompleteFindings.length} incomplete finding(s) require manual review.`);
999
1005
 
1000
- writeJson(args.output, {
1006
+ const outputPayload = {
1001
1007
  ...result,
1002
1008
  findings: dedupedFindings,
1003
1009
  incomplete_findings: incompleteFindings,
@@ -1011,12 +1017,32 @@ function main() {
1011
1017
  fpFiltered: fpRemovedCount,
1012
1018
  deduplicatedCount,
1013
1019
  },
1014
- });
1020
+ };
1021
+
1022
+ // Write to disk for CLI compatibility
1023
+ writeJson(args.output, outputPayload);
1015
1024
 
1016
1025
  if (dedupedFindings.length === 0) {
1017
1026
  log.info("Congratulations, no issues found.");
1018
1027
  }
1019
1028
  log.success(`Findings processed and saved to ${args.output}`);
1029
+
1030
+ return outputPayload;
1031
+ }
1032
+
1033
+ /**
1034
+ * CLI entry point — reads from disk, processes, writes to disk.
1035
+ */
1036
+ function main() {
1037
+ const args = parseArgs(process.argv.slice(2));
1038
+ const payload = readJson(args.input);
1039
+ if (!payload) throw new Error(`Input not found or invalid: ${args.input}`);
1040
+
1041
+ runAnalyzer(payload, {
1042
+ ignoreFindings: args.ignoreFindings,
1043
+ framework: args.framework,
1044
+ output: args.output,
1045
+ });
1020
1046
  }
1021
1047
 
1022
1048
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -489,7 +489,16 @@ async function analyzeRoute(
489
489
  * @param {"pending"|"running"|"done"|"error"} status - Step status.
490
490
  * @param {Object} [extra={}] - Additional metadata.
491
491
  */
492
+ /** @type {((step: string, status: string, extra?: object) => void) | null} */
493
+ let _onProgressCallback = null;
494
+
492
495
  function writeProgress(step, status, extra = {}) {
496
+ // Notify external callback if set (programmatic API)
497
+ if (_onProgressCallback) {
498
+ _onProgressCallback(step, status, extra);
499
+ }
500
+
501
+ // Always write to disk for CLI consumers
493
502
  const progressPath = getInternalPath("progress.json");
494
503
  let progress = {};
495
504
  try {
@@ -788,8 +797,45 @@ function mergeViolations(axeViolations, cdpViolations, pa11yViolations) {
788
797
  * Coordinates browser setup, crawling/discovery, parallel scanning, and result saving.
789
798
  * @throws {Error} If navigation to the base URL fails or browser setup issues occur.
790
799
  */
791
- async function main() {
792
- const args = parseArgs(process.argv.slice(2));
800
+ /**
801
+ * Runs the DOM scanner programmatically.
802
+ * @param {Object} options - Scanner configuration (same shape as CLI args object).
803
+ * @param {{ onProgress?: (step: string, status: string, extra?: object) => void }} [callbacks={}]
804
+ * @returns {Promise<Object>} The scan payload { generated_at, base_url, onlyRule, projectContext, routes }.
805
+ */
806
+ export async function runDomScanner(options = {}, callbacks = {}) {
807
+ const args = {
808
+ baseUrl: options.baseUrl || "",
809
+ routes: options.routes || "",
810
+ output: options.output || getInternalPath("a11y-scan-results.json"),
811
+ maxRoutes: options.maxRoutes ?? DEFAULTS.maxRoutes,
812
+ waitMs: options.waitMs ?? DEFAULTS.waitMs,
813
+ timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,
814
+ headless: options.headless ?? DEFAULTS.headless,
815
+ waitUntil: options.waitUntil ?? DEFAULTS.waitUntil,
816
+ colorScheme: options.colorScheme || null,
817
+ screenshotsDir: options.screenshotsDir || getInternalPath("screenshots"),
818
+ excludeSelectors: options.excludeSelectors || [],
819
+ onlyRule: options.onlyRule || null,
820
+ crawlDepth: Math.min(Math.max(options.crawlDepth ?? DEFAULTS.crawlDepth, 1), 3),
821
+ viewport: options.viewport || null,
822
+ axeTags: options.axeTags || null,
823
+ };
824
+
825
+ if (!args.baseUrl) throw new Error("Missing required option: baseUrl");
826
+
827
+ if (callbacks.onProgress) {
828
+ _onProgressCallback = callbacks.onProgress;
829
+ }
830
+
831
+ try {
832
+ return await _runDomScannerInternal(args);
833
+ } finally {
834
+ _onProgressCallback = null;
835
+ }
836
+ }
837
+
838
+ async function _runDomScannerInternal(args) {
793
839
  const baseUrl = new URL(args.baseUrl).toString();
794
840
  const origin = new URL(baseUrl).origin;
795
841
 
@@ -850,7 +896,7 @@ async function main() {
850
896
  } catch (err) {
851
897
  log.error(`Fatal: Could not load base URL ${baseUrl}: ${err.message}`);
852
898
  await browser.close();
853
- process.exit(1);
899
+ throw new Error(`Could not load base URL ${baseUrl}: ${err.message}`);
854
900
  }
855
901
 
856
902
  /**
@@ -1036,6 +1082,13 @@ async function main() {
1036
1082
 
1037
1083
  writeJson(args.output, payload);
1038
1084
  log.success(`Routes scan complete. Results saved to ${args.output}`);
1085
+
1086
+ return payload;
1087
+ }
1088
+
1089
+ async function main() {
1090
+ const args = parseArgs(process.argv.slice(2));
1091
+ await runDomScanner(args);
1039
1092
  }
1040
1093
 
1041
1094
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -113,7 +113,7 @@ function walkFiles(dir, extensions, results = []) {
113
113
  * @param {string} projectDir
114
114
  * @returns {string[]}
115
115
  */
116
- function resolveScanDirs(framework, projectDir) {
116
+ export function resolveScanDirs(framework, projectDir) {
117
117
  const boundaries = framework ? SOURCE_BOUNDARIES?.[framework] : null;
118
118
  if (!boundaries) return [projectDir];
119
119
 
@@ -63,7 +63,28 @@ export interface EnrichedFinding extends Finding {
63
63
  fixDifficultyNotes: string | string[] | null;
64
64
  screenshotPath: string | null;
65
65
  wcagCriterionId: string | null;
66
+ wcagClassification: string | null;
66
67
  impactedUsers: string | null;
68
+ primarySelector: string;
69
+ primaryFailureMode: string | null;
70
+ relationshipHint: string | null;
71
+ failureChecks: unknown[];
72
+ relatedContext: unknown[];
73
+ recommendedFix: string;
74
+ totalInstances: number | null;
75
+ relatedRules: string[];
76
+ ownershipStatus: string;
77
+ ownershipReason: string | null;
78
+ primarySourceScope: string[];
79
+ searchStrategy: string;
80
+ managedByLibrary: string | null;
81
+ componentHint: string | null;
82
+ verificationCommand: string | null;
83
+ verificationCommandFallback: string | null;
84
+ checkData: Record<string, unknown> | null;
85
+ pagesAffected: number | null;
86
+ affectedUrls: string[] | null;
87
+ effort: string;
67
88
  }
68
89
 
69
90
  export interface SeverityTotals {
@@ -79,12 +100,22 @@ export interface PersonaGroup {
79
100
  icon: string;
80
101
  }
81
102
 
103
+ export interface DetectedStack {
104
+ framework: string | null;
105
+ cms: string | null;
106
+ uiLibraries: string[];
107
+ }
108
+
82
109
  export interface AuditSummary {
83
110
  totals: SeverityTotals;
84
111
  score: number;
85
112
  label: string;
86
113
  wcagStatus: "Pass" | "Conditional Pass" | "Fail";
87
114
  personaGroups: Record<string, PersonaGroup>;
115
+ quickWins: EnrichedFinding[];
116
+ targetUrl: string;
117
+ detectedStack: DetectedStack;
118
+ totalFindings: number;
88
119
  }
89
120
 
90
121
  // ---------------------------------------------------------------------------
@@ -106,19 +137,109 @@ export interface PDFReport {
106
137
  contentType: "application/pdf";
107
138
  }
108
139
 
140
+ export interface HTMLReport {
141
+ html: string;
142
+ contentType: "text/html";
143
+ }
144
+
109
145
  export interface ChecklistReport {
110
146
  html: string;
111
147
  contentType: "text/html";
112
148
  }
113
149
 
150
+ export interface RemediationGuide {
151
+ markdown: string;
152
+ contentType: "text/markdown";
153
+ }
154
+
155
+ export interface SourcePatternFinding {
156
+ id: string;
157
+ pattern_id: string;
158
+ title: string;
159
+ severity: string;
160
+ wcag: string;
161
+ wcag_criterion: string;
162
+ wcag_level: string;
163
+ type: string;
164
+ fix_description: string | null;
165
+ status: "confirmed" | "potential";
166
+ file: string;
167
+ line: number;
168
+ match: string;
169
+ context: string;
170
+ source: "code-pattern";
171
+ }
172
+
173
+ export interface SourcePatternResult {
174
+ findings: SourcePatternFinding[];
175
+ summary: {
176
+ total: number;
177
+ confirmed: number;
178
+ potential: number;
179
+ };
180
+ }
181
+
182
+ export interface HTMLReportOptions extends ReportOptions {
183
+ screenshotsDir?: string;
184
+ }
185
+
186
+ export interface RemediationOptions extends ReportOptions {
187
+ patternFindings?: Record<string, unknown> | null;
188
+ }
189
+
190
+ export interface SourcePatternOptions {
191
+ framework?: string;
192
+ onlyPattern?: string;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Audit options
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export interface RunAuditOptions {
200
+ baseUrl: string;
201
+ maxRoutes?: number;
202
+ crawlDepth?: number;
203
+ routes?: string;
204
+ waitMs?: number;
205
+ timeoutMs?: number;
206
+ headless?: boolean;
207
+ waitUntil?: string;
208
+ colorScheme?: string;
209
+ viewport?: { width: number; height: number };
210
+ axeTags?: string[];
211
+ onlyRule?: string;
212
+ excludeSelectors?: string[];
213
+ ignoreFindings?: string[];
214
+ framework?: string;
215
+ projectDir?: string;
216
+ skipPatterns?: boolean;
217
+ onProgress?: (step: string, status: string, extra?: Record<string, unknown>) => void;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Enrichment options
222
+ // ---------------------------------------------------------------------------
223
+
224
+ export interface EnrichmentOptions {
225
+ screenshotUrlBuilder?: (rawPath: string) => string;
226
+ }
227
+
114
228
  // ---------------------------------------------------------------------------
115
229
  // Public API
116
230
  // ---------------------------------------------------------------------------
117
231
 
118
- export function getEnrichedFindings(findings: Finding[]): EnrichedFinding[];
119
- export function getEnrichedFindings(findings: Record<string, unknown>[]): EnrichedFinding[];
232
+ export function runAudit(options: RunAuditOptions): Promise<ScanPayload>;
233
+
234
+ export function getEnrichedFindings(
235
+ input: ScanPayload | Finding[] | Record<string, unknown>[],
236
+ options?: EnrichmentOptions
237
+ ): EnrichedFinding[];
120
238
 
121
- export function getAuditSummary(findings: EnrichedFinding[] | Record<string, unknown>[]): AuditSummary;
239
+ export function getAuditSummary(
240
+ findings: EnrichedFinding[],
241
+ payload?: ScanPayload | null
242
+ ): AuditSummary;
122
243
 
123
244
  export function getPDFReport(
124
245
  payload: ScanPayload,
@@ -128,3 +249,18 @@ export function getPDFReport(
128
249
  export function getChecklist(
129
250
  options?: Pick<ReportOptions, "baseUrl">
130
251
  ): Promise<ChecklistReport>;
252
+
253
+ export function getHTMLReport(
254
+ payload: ScanPayload,
255
+ options?: HTMLReportOptions
256
+ ): Promise<HTMLReport>;
257
+
258
+ export function getRemediationGuide(
259
+ payload: ScanPayload & { incomplete_findings?: unknown[] },
260
+ options?: RemediationOptions
261
+ ): Promise<RemediationGuide>;
262
+
263
+ export function getSourcePatterns(
264
+ projectDir: string,
265
+ options?: SourcePatternOptions
266
+ ): Promise<SourcePatternResult>;