@diegovelasquezweb/a11y-engine 0.1.7 → 0.1.9
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/package.json +1 -1
- package/scripts/index.d.mts +59 -13
- package/scripts/index.mjs +238 -80
package/package.json
CHANGED
package/scripts/index.d.mts
CHANGED
|
@@ -58,8 +58,33 @@ export interface EnrichedFinding extends Finding {
|
|
|
58
58
|
sourceRuleId: string | null;
|
|
59
59
|
fixDescription: string | null;
|
|
60
60
|
fixCode: string | null;
|
|
61
|
+
fixCodeLang: string | null;
|
|
61
62
|
falsePositiveRisk: string | null;
|
|
62
63
|
fixDifficultyNotes: string | string[] | null;
|
|
64
|
+
screenshotPath: string | null;
|
|
65
|
+
wcagCriterionId: string | null;
|
|
66
|
+
wcagClassification: string | null;
|
|
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;
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
export interface SeverityTotals {
|
|
@@ -69,18 +94,30 @@ export interface SeverityTotals {
|
|
|
69
94
|
Minor: number;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
|
-
export interface ComplianceScore {
|
|
73
|
-
score: number;
|
|
74
|
-
label: string;
|
|
75
|
-
wcagStatus: "Pass" | "Conditional Pass" | "Fail";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
97
|
export interface PersonaGroup {
|
|
79
98
|
label: string;
|
|
80
99
|
count: number;
|
|
81
100
|
icon: string;
|
|
82
101
|
}
|
|
83
102
|
|
|
103
|
+
export interface DetectedStack {
|
|
104
|
+
framework: string | null;
|
|
105
|
+
cms: string | null;
|
|
106
|
+
uiLibraries: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface AuditSummary {
|
|
110
|
+
totals: SeverityTotals;
|
|
111
|
+
score: number;
|
|
112
|
+
label: string;
|
|
113
|
+
wcagStatus: "Pass" | "Conditional Pass" | "Fail";
|
|
114
|
+
personaGroups: Record<string, PersonaGroup>;
|
|
115
|
+
quickWins: EnrichedFinding[];
|
|
116
|
+
targetUrl: string;
|
|
117
|
+
detectedStack: DetectedStack;
|
|
118
|
+
totalFindings: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
84
121
|
// ---------------------------------------------------------------------------
|
|
85
122
|
// Report types
|
|
86
123
|
// ---------------------------------------------------------------------------
|
|
@@ -106,17 +143,26 @@ export interface ChecklistReport {
|
|
|
106
143
|
}
|
|
107
144
|
|
|
108
145
|
// ---------------------------------------------------------------------------
|
|
109
|
-
//
|
|
146
|
+
// Enrichment options
|
|
110
147
|
// ---------------------------------------------------------------------------
|
|
111
148
|
|
|
112
|
-
export
|
|
113
|
-
|
|
149
|
+
export interface EnrichmentOptions {
|
|
150
|
+
screenshotUrlBuilder?: (rawPath: string) => string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Public API
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
114
156
|
|
|
115
|
-
export function
|
|
157
|
+
export function getEnrichedFindings(
|
|
158
|
+
input: ScanPayload | Finding[] | Record<string, unknown>[],
|
|
159
|
+
options?: EnrichmentOptions
|
|
160
|
+
): EnrichedFinding[];
|
|
116
161
|
|
|
117
|
-
export function
|
|
118
|
-
findings: EnrichedFinding[]
|
|
119
|
-
|
|
162
|
+
export function getAuditSummary(
|
|
163
|
+
findings: EnrichedFinding[],
|
|
164
|
+
payload?: ScanPayload | null
|
|
165
|
+
): AuditSummary;
|
|
120
166
|
|
|
121
167
|
export function getPDFReport(
|
|
122
168
|
payload: ScanPayload,
|
package/scripts/index.mjs
CHANGED
|
@@ -42,31 +42,9 @@ function getWcagReference() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
|
-
//
|
|
45
|
+
// Pa11y rule canonicalization (internal)
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Returns all engine asset data. Lazy-loaded and cached.
|
|
50
|
-
* @returns {{ intelligence: object, pa11yConfig: object, complianceConfig: object, wcagReference: object }}
|
|
51
|
-
*/
|
|
52
|
-
function getAssets() {
|
|
53
|
-
return {
|
|
54
|
-
intelligence: getIntelligence(),
|
|
55
|
-
pa11yConfig: getPa11yConfig(),
|
|
56
|
-
complianceConfig: getComplianceConfig(),
|
|
57
|
-
wcagReference: getWcagReference(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Pa11y rule canonicalization
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Normalizes a pa11y code token for comparison.
|
|
67
|
-
* @param {string} value
|
|
68
|
-
* @returns {string}
|
|
69
|
-
*/
|
|
70
48
|
function normalizePa11yToken(value) {
|
|
71
49
|
return value
|
|
72
50
|
.toLowerCase()
|
|
@@ -75,13 +53,6 @@ function normalizePa11yToken(value) {
|
|
|
75
53
|
.replace(/[^a-z0-9]/g, "");
|
|
76
54
|
}
|
|
77
55
|
|
|
78
|
-
/**
|
|
79
|
-
* Maps a pa11y rule ID to its canonical axe-equivalent ID.
|
|
80
|
-
* @param {string} ruleId - The current rule ID (may already be canonical or a pa11y slug).
|
|
81
|
-
* @param {string|null} sourceRuleId - The original pa11y code if available.
|
|
82
|
-
* @param {object|null} checkData - The check_data object which may contain a `code` field.
|
|
83
|
-
* @returns {string} The canonical rule ID (e.g., "color-contrast").
|
|
84
|
-
*/
|
|
85
56
|
function mapPa11yRuleToCanonical(ruleId, sourceRuleId = null, checkData = null) {
|
|
86
57
|
const equivalenceMap = getPa11yConfig().equivalenceMap || {};
|
|
87
58
|
|
|
@@ -109,66 +80,203 @@ function mapPa11yRuleToCanonical(ruleId, sourceRuleId = null, checkData = null)
|
|
|
109
80
|
return ruleId;
|
|
110
81
|
}
|
|
111
82
|
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Raw finding normalization (internal)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const SEVERITY_ORDER = { Critical: 1, Serious: 2, Moderate: 3, Minor: 4 };
|
|
88
|
+
|
|
89
|
+
function str(v, fallback = "") {
|
|
90
|
+
return typeof v === "string" ? v : (v != null ? String(v) : fallback);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function strOrNull(v) {
|
|
94
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeSingleFinding(item, index, screenshotUrlBuilder) {
|
|
98
|
+
const screenshotRaw = strOrNull(item.screenshot_path);
|
|
99
|
+
const screenshotPath = screenshotRaw && screenshotUrlBuilder
|
|
100
|
+
? screenshotUrlBuilder(screenshotRaw)
|
|
101
|
+
: screenshotRaw;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id: str(item.id, `A11Y-${String(index + 1).padStart(3, "0")}`),
|
|
105
|
+
rule_id: str(item.rule_id),
|
|
106
|
+
source: str(item.source, "axe"),
|
|
107
|
+
source_rule_id: strOrNull(item.source_rule_id),
|
|
108
|
+
wcag_criterion_id: strOrNull(item.wcag_criterion_id),
|
|
109
|
+
category: strOrNull(item.category),
|
|
110
|
+
title: str(item.title, "Untitled finding"),
|
|
111
|
+
severity: str(item.severity, "Unknown"),
|
|
112
|
+
wcag: str(item.wcag),
|
|
113
|
+
wcag_classification: strOrNull(item.wcag_classification),
|
|
114
|
+
area: str(item.area),
|
|
115
|
+
url: str(item.url),
|
|
116
|
+
selector: str(item.selector),
|
|
117
|
+
primary_selector: str(item.primary_selector || item.selector),
|
|
118
|
+
impacted_users: str(item.impacted_users),
|
|
119
|
+
actual: str(item.actual),
|
|
120
|
+
primary_failure_mode: strOrNull(item.primary_failure_mode),
|
|
121
|
+
relationship_hint: strOrNull(item.relationship_hint),
|
|
122
|
+
failure_checks: Array.isArray(item.failure_checks) ? item.failure_checks : [],
|
|
123
|
+
related_context: Array.isArray(item.related_context) ? item.related_context : [],
|
|
124
|
+
expected: str(item.expected),
|
|
125
|
+
mdn: strOrNull(item.mdn),
|
|
126
|
+
fix_description: strOrNull(item.fix_description),
|
|
127
|
+
fix_code: strOrNull(item.fix_code),
|
|
128
|
+
fix_code_lang: str(item.fix_code_lang, "html"),
|
|
129
|
+
recommended_fix: str(item.recommended_fix),
|
|
130
|
+
evidence: Array.isArray(item.evidence) ? item.evidence : [],
|
|
131
|
+
total_instances: typeof item.total_instances === "number" ? item.total_instances : null,
|
|
132
|
+
effort: strOrNull(item.effort), // null = will be inferred during enrichment
|
|
133
|
+
related_rules: Array.isArray(item.related_rules) ? item.related_rules : [],
|
|
134
|
+
screenshot_path: screenshotPath,
|
|
135
|
+
false_positive_risk: strOrNull(item.false_positive_risk),
|
|
136
|
+
guardrails: item.guardrails && typeof item.guardrails === "object" ? item.guardrails : null,
|
|
137
|
+
fix_difficulty_notes: item.fix_difficulty_notes ?? null,
|
|
138
|
+
framework_notes: strOrNull(item.framework_notes),
|
|
139
|
+
cms_notes: strOrNull(item.cms_notes),
|
|
140
|
+
file_search_pattern: strOrNull(item.file_search_pattern),
|
|
141
|
+
ownership_status: str(item.ownership_status, "unknown"),
|
|
142
|
+
ownership_reason: strOrNull(item.ownership_reason),
|
|
143
|
+
primary_source_scope: Array.isArray(item.primary_source_scope) ? item.primary_source_scope : [],
|
|
144
|
+
search_strategy: str(item.search_strategy, "verify_ownership_before_search"),
|
|
145
|
+
managed_by_library: strOrNull(item.managed_by_library),
|
|
146
|
+
component_hint: strOrNull(item.component_hint),
|
|
147
|
+
verification_command: strOrNull(item.verification_command),
|
|
148
|
+
verification_command_fallback: strOrNull(item.verification_command_fallback),
|
|
149
|
+
check_data: item.check_data && typeof item.check_data === "object" ? item.check_data : null,
|
|
150
|
+
pages_affected: typeof item.pages_affected === "number" ? item.pages_affected : null,
|
|
151
|
+
affected_urls: Array.isArray(item.affected_urls) ? item.affected_urls : null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
112
155
|
// ---------------------------------------------------------------------------
|
|
113
156
|
// Finding enrichment
|
|
114
157
|
// ---------------------------------------------------------------------------
|
|
115
158
|
|
|
116
159
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
160
|
+
* Normalizes and enriches raw findings with intelligence data.
|
|
161
|
+
*
|
|
162
|
+
* Accepts either:
|
|
163
|
+
* - A full scan payload: { findings: object[], metadata?: object }
|
|
164
|
+
* - An array of findings directly: object[]
|
|
165
|
+
*
|
|
166
|
+
* Options:
|
|
167
|
+
* - screenshotUrlBuilder: (rawPath: string) => string — transforms screenshot
|
|
168
|
+
* paths into consumer-specific URLs.
|
|
169
|
+
*
|
|
170
|
+
* @param {object[]|{findings: object[]}} input
|
|
171
|
+
* @param {{ screenshotUrlBuilder?: (path: string) => string }} [options={}]
|
|
172
|
+
* @returns {object[]} Enriched, normalized, sorted findings.
|
|
121
173
|
*/
|
|
122
|
-
export function getEnrichedFindings(
|
|
174
|
+
export function getEnrichedFindings(input, options = {}) {
|
|
175
|
+
const { screenshotUrlBuilder = null } = options;
|
|
123
176
|
const rules = getIntelligence().rules || {};
|
|
124
177
|
|
|
125
|
-
|
|
178
|
+
// Accept payload object or array directly
|
|
179
|
+
const rawFindings = Array.isArray(input) ? input : (input?.findings || []);
|
|
180
|
+
|
|
181
|
+
// Normalize raw findings
|
|
182
|
+
const normalized = rawFindings.map((item, index) =>
|
|
183
|
+
normalizeSingleFinding(item, index, screenshotUrlBuilder)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Enrich with intelligence + camelCase aliases
|
|
187
|
+
const enriched = normalized.map((finding) => {
|
|
126
188
|
const canonical = mapPa11yRuleToCanonical(
|
|
127
|
-
finding.
|
|
128
|
-
finding.
|
|
129
|
-
finding.
|
|
189
|
+
finding.rule_id,
|
|
190
|
+
finding.source_rule_id,
|
|
191
|
+
finding.check_data,
|
|
130
192
|
);
|
|
131
193
|
|
|
132
|
-
|
|
194
|
+
// Effort will be inferred after enrichment
|
|
195
|
+
|
|
196
|
+
// Always create camelCase aliases
|
|
197
|
+
const withAliases = {
|
|
133
198
|
...finding,
|
|
134
199
|
ruleId: canonical,
|
|
135
200
|
rule_id: canonical,
|
|
136
|
-
sourceRuleId: finding.
|
|
201
|
+
sourceRuleId: finding.source_rule_id || finding.rule_id || null,
|
|
202
|
+
fixDescription: finding.fix_description,
|
|
203
|
+
fixCode: finding.fix_code,
|
|
204
|
+
fixCodeLang: finding.fix_code_lang,
|
|
205
|
+
falsePositiveRisk: finding.false_positive_risk,
|
|
206
|
+
fixDifficultyNotes: finding.fix_difficulty_notes,
|
|
207
|
+
screenshotPath: finding.screenshot_path,
|
|
208
|
+
wcagCriterionId: finding.wcag_criterion_id,
|
|
209
|
+
wcagClassification: finding.wcag_classification,
|
|
210
|
+
impactedUsers: finding.impacted_users,
|
|
211
|
+
primarySelector: finding.primary_selector,
|
|
212
|
+
primaryFailureMode: finding.primary_failure_mode,
|
|
213
|
+
relationshipHint: finding.relationship_hint,
|
|
214
|
+
failureChecks: finding.failure_checks,
|
|
215
|
+
relatedContext: finding.related_context,
|
|
216
|
+
recommendedFix: finding.recommended_fix,
|
|
217
|
+
totalInstances: finding.total_instances,
|
|
218
|
+
relatedRules: finding.related_rules,
|
|
219
|
+
ownershipStatus: finding.ownership_status,
|
|
220
|
+
ownershipReason: finding.ownership_reason,
|
|
221
|
+
primarySourceScope: finding.primary_source_scope,
|
|
222
|
+
searchStrategy: finding.search_strategy,
|
|
223
|
+
managedByLibrary: finding.managed_by_library,
|
|
224
|
+
componentHint: finding.component_hint,
|
|
225
|
+
verificationCommand: finding.verification_command,
|
|
226
|
+
verificationCommandFallback: finding.verification_command_fallback,
|
|
227
|
+
checkData: finding.check_data,
|
|
228
|
+
pagesAffected: finding.pages_affected,
|
|
229
|
+
affectedUrls: finding.affected_urls,
|
|
137
230
|
};
|
|
138
231
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
232
|
+
// If fix data already exists, no need to look up intelligence
|
|
233
|
+
let result;
|
|
234
|
+
if (withAliases.fixDescription || withAliases.fixCode) {
|
|
235
|
+
result = withAliases;
|
|
236
|
+
} else {
|
|
237
|
+
const info = rules[canonical];
|
|
238
|
+
if (!info) {
|
|
239
|
+
result = withAliases;
|
|
240
|
+
} else {
|
|
241
|
+
result = {
|
|
242
|
+
...withAliases,
|
|
243
|
+
category: withAliases.category ?? info.category ?? null,
|
|
244
|
+
fixDescription: info.fix?.description ?? null,
|
|
245
|
+
fix_description: info.fix?.description ?? null,
|
|
246
|
+
fixCode: info.fix?.code ?? null,
|
|
247
|
+
fix_code: info.fix?.code ?? withAliases.fix_code ?? null,
|
|
248
|
+
falsePositiveRisk: withAliases.falsePositiveRisk ?? info.false_positive_risk ?? null,
|
|
249
|
+
false_positive_risk: withAliases.false_positive_risk ?? info.false_positive_risk ?? null,
|
|
250
|
+
fixDifficultyNotes: withAliases.fixDifficultyNotes ?? info.fix_difficulty_notes ?? null,
|
|
251
|
+
fix_difficulty_notes: withAliases.fix_difficulty_notes ?? info.fix_difficulty_notes ?? null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
142
254
|
}
|
|
143
255
|
|
|
144
|
-
|
|
145
|
-
if (!
|
|
256
|
+
// Infer effort AFTER enrichment so intelligence-provided fixCode is considered
|
|
257
|
+
if (!result.effort || result.effort === "null") {
|
|
258
|
+
result.effort = (result.fixCode || result.fix_code) ? "low" : "high";
|
|
259
|
+
}
|
|
146
260
|
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
fixDifficultyNotes: normalized.fixDifficultyNotes ?? normalized.fix_difficulty_notes ?? info.fix_difficulty_notes ?? null,
|
|
157
|
-
fix_difficulty_notes: normalized.fix_difficulty_notes ?? info.fix_difficulty_notes ?? null,
|
|
158
|
-
};
|
|
261
|
+
return result;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Sort by severity then by ID
|
|
265
|
+
enriched.sort((a, b) => {
|
|
266
|
+
const sa = SEVERITY_ORDER[a.severity] ?? 99;
|
|
267
|
+
const sb = SEVERITY_ORDER[b.severity] ?? 99;
|
|
268
|
+
if (sa !== sb) return sa - sb;
|
|
269
|
+
return a.id.localeCompare(b.id);
|
|
159
270
|
});
|
|
271
|
+
|
|
272
|
+
return enriched;
|
|
160
273
|
}
|
|
161
274
|
|
|
162
275
|
// ---------------------------------------------------------------------------
|
|
163
|
-
// Score computation
|
|
276
|
+
// Score computation (internal)
|
|
164
277
|
// ---------------------------------------------------------------------------
|
|
165
278
|
|
|
166
|
-
|
|
167
|
-
* Computes compliance score, grade label, and WCAG pass/fail status.
|
|
168
|
-
* @param {{ Critical: number, Serious: number, Moderate: number, Minor: number }} totals
|
|
169
|
-
* @returns {{ score: number, label: string, wcagStatus: "Pass" | "Conditional Pass" | "Fail" }}
|
|
170
|
-
*/
|
|
171
|
-
export function getComplianceScore(totals) {
|
|
279
|
+
function getComplianceScore(totals) {
|
|
172
280
|
const config = getComplianceConfig();
|
|
173
281
|
const penalties = config.complianceScore.penalties;
|
|
174
282
|
const thresholds = config.gradeThresholds;
|
|
@@ -198,15 +306,10 @@ export function getComplianceScore(totals) {
|
|
|
198
306
|
}
|
|
199
307
|
|
|
200
308
|
// ---------------------------------------------------------------------------
|
|
201
|
-
// Persona grouping
|
|
309
|
+
// Persona grouping (internal)
|
|
202
310
|
// ---------------------------------------------------------------------------
|
|
203
311
|
|
|
204
|
-
|
|
205
|
-
* Groups findings by accessibility persona (screen reader, keyboard, cognitive, etc.).
|
|
206
|
-
* @param {object[]} findings - Array of findings with ruleId, wcagCriterionId, impactedUsers.
|
|
207
|
-
* @returns {Record<string, { label: string, count: number, icon: string }>}
|
|
208
|
-
*/
|
|
209
|
-
export function getPersonaGroups(findings) {
|
|
312
|
+
function getPersonaGroups(findings) {
|
|
210
313
|
const ref = getWcagReference();
|
|
211
314
|
const personaConfig = ref.personaConfig || {};
|
|
212
315
|
const personaMapping = ref.personaMapping || {};
|
|
@@ -261,6 +364,66 @@ export function getPersonaGroups(findings) {
|
|
|
261
364
|
return groups;
|
|
262
365
|
}
|
|
263
366
|
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Audit summary
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Computes a complete audit summary from enriched findings: severity totals,
|
|
373
|
+
* compliance score, grade label, WCAG status, persona groups, quick wins,
|
|
374
|
+
* target URL, and detected stack.
|
|
375
|
+
*
|
|
376
|
+
* @param {object[]} findings - Array of enriched findings.
|
|
377
|
+
* @param {{ findings: object[], metadata?: object }|null} [payload=null] - Original scan payload for metadata extraction.
|
|
378
|
+
* @returns {object} Full audit summary.
|
|
379
|
+
*/
|
|
380
|
+
export function getAuditSummary(findings, payload = null) {
|
|
381
|
+
const totals = { Critical: 0, Serious: 0, Moderate: 0, Minor: 0 };
|
|
382
|
+
for (const f of findings) {
|
|
383
|
+
const severity = f.severity || "";
|
|
384
|
+
if (severity in totals) totals[severity] += 1;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { score, label, wcagStatus } = getComplianceScore(totals);
|
|
388
|
+
const personaGroups = getPersonaGroups(findings);
|
|
389
|
+
|
|
390
|
+
const quickWins = findings
|
|
391
|
+
.filter((f) =>
|
|
392
|
+
(f.severity === "Critical" || f.severity === "Serious") &&
|
|
393
|
+
(f.fixCode || f.fix_code)
|
|
394
|
+
)
|
|
395
|
+
.slice(0, 3);
|
|
396
|
+
|
|
397
|
+
// Extract metadata from payload if provided
|
|
398
|
+
let targetUrl = "";
|
|
399
|
+
let detectedStack = { framework: null, cms: null, uiLibraries: [] };
|
|
400
|
+
|
|
401
|
+
if (payload && payload.metadata) {
|
|
402
|
+
const meta = payload.metadata;
|
|
403
|
+
const firstUrl = findings.length > 0 ? str(findings[0].url) : "";
|
|
404
|
+
targetUrl = str(meta.target_url || meta.targetUrl || meta.base_url || firstUrl);
|
|
405
|
+
|
|
406
|
+
const ctx = meta.projectContext || {};
|
|
407
|
+
detectedStack = {
|
|
408
|
+
framework: strOrNull(ctx.framework),
|
|
409
|
+
cms: strOrNull(ctx.cms),
|
|
410
|
+
uiLibraries: Array.isArray(ctx.uiLibraries) ? ctx.uiLibraries : [],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
totals,
|
|
416
|
+
score,
|
|
417
|
+
label,
|
|
418
|
+
wcagStatus,
|
|
419
|
+
personaGroups,
|
|
420
|
+
quickWins,
|
|
421
|
+
targetUrl,
|
|
422
|
+
detectedStack,
|
|
423
|
+
totalFindings: findings.length,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
264
427
|
// ---------------------------------------------------------------------------
|
|
265
428
|
// Report generation
|
|
266
429
|
// ---------------------------------------------------------------------------
|
|
@@ -276,10 +439,9 @@ import {
|
|
|
276
439
|
|
|
277
440
|
/**
|
|
278
441
|
* Generates a PDF report buffer from raw scan findings.
|
|
279
|
-
*
|
|
280
|
-
* @param {{ findings: object[], metadata?: object }} payload - Raw scan output (snake_case keys).
|
|
442
|
+
* @param {{ findings: object[], metadata?: object }} payload
|
|
281
443
|
* @param {{ baseUrl?: string, target?: string }} [options={}]
|
|
282
|
-
* @returns {Promise<
|
|
444
|
+
* @returns {Promise<{ buffer: Buffer, contentType: "application/pdf" }>}
|
|
283
445
|
*/
|
|
284
446
|
export async function getPDFReport(payload, options = {}) {
|
|
285
447
|
const { chromium } = await import("playwright");
|
|
@@ -345,9 +507,8 @@ ${buildPdfAuditLimitations()}
|
|
|
345
507
|
|
|
346
508
|
/**
|
|
347
509
|
* Generates a standalone manual accessibility checklist HTML string.
|
|
348
|
-
* Does not depend on scan results — reads from manual-checks.json asset.
|
|
349
510
|
* @param {{ baseUrl?: string }} [options={}]
|
|
350
|
-
* @returns {Promise<
|
|
511
|
+
* @returns {Promise<{ html: string, contentType: "text/html" }>}
|
|
351
512
|
*/
|
|
352
513
|
export async function getChecklist(options = {}) {
|
|
353
514
|
const { buildManualCheckCard } = await import("./reports/renderers/html.mjs");
|
|
@@ -365,9 +526,6 @@ export async function getChecklist(options = {}) {
|
|
|
365
526
|
const selectClasses =
|
|
366
527
|
"pl-4 pr-10 py-3 bg-white border border-slate-300 rounded-2xl text-sm font-bold text-slate-800 focus:outline-none focus:ring-4 focus:ring-amber-500/20 focus:border-amber-400 shadow-sm transition-all appearance-none cursor-pointer bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20stroke%3D%22%23374151%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%221.5%22%3E%3Cpath%20d%3D%22m6%208%204%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-[length:1.25rem_1.25rem] bg-[right_0.5rem_center] bg-no-repeat";
|
|
367
528
|
|
|
368
|
-
// Import the full checklist builder to reuse its buildHtml
|
|
369
|
-
// The checklist builder module has a main() that auto-runs, so we dynamically
|
|
370
|
-
// construct the same output using the renderer functions directly.
|
|
371
529
|
const html = `<!doctype html>
|
|
372
530
|
<html lang="en">
|
|
373
531
|
<head>
|